@papi-ai/server 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +819 -47
- package/dist/prompts.js +236 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6071,6 +6071,38 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6071
6071
|
count: parseInt(r.count, 10)
|
|
6072
6072
|
}));
|
|
6073
6073
|
}
|
|
6074
|
+
async getModuleEstimationStats() {
|
|
6075
|
+
const rows = await this.sql`
|
|
6076
|
+
SELECT
|
|
6077
|
+
ct.module,
|
|
6078
|
+
COUNT(*)::text AS total,
|
|
6079
|
+
COUNT(*) FILTER (WHERE br.actual_effort = br.estimated_effort)::text AS accurate_count,
|
|
6080
|
+
COUNT(*) FILTER (WHERE br.scope_accuracy = 'accurate')::text AS scope_accurate
|
|
6081
|
+
FROM build_reports br
|
|
6082
|
+
JOIN cycle_tasks ct ON br.task_id = ct.uuid
|
|
6083
|
+
WHERE br.project_id = ${this.projectId}
|
|
6084
|
+
AND ct.project_id = ${this.projectId}
|
|
6085
|
+
AND br.cycle > (SELECT COALESCE(MAX(cycle), 0) FROM build_reports WHERE project_id = ${this.projectId}) - 20
|
|
6086
|
+
AND br.actual_effort IS NOT NULL
|
|
6087
|
+
AND br.estimated_effort IS NOT NULL
|
|
6088
|
+
AND ct.module IS NOT NULL
|
|
6089
|
+
AND ct.module != ''
|
|
6090
|
+
GROUP BY ct.module
|
|
6091
|
+
HAVING COUNT(*) >= 3
|
|
6092
|
+
ORDER BY COUNT(*) DESC
|
|
6093
|
+
`;
|
|
6094
|
+
return rows.map((r) => {
|
|
6095
|
+
const total = parseInt(r.total, 10);
|
|
6096
|
+
const accurate = parseInt(r.accurate_count, 10);
|
|
6097
|
+
const scopeAccurate = parseInt(r.scope_accurate, 10);
|
|
6098
|
+
return {
|
|
6099
|
+
module: r.module,
|
|
6100
|
+
total,
|
|
6101
|
+
accuracyPct: total > 0 ? Math.round(accurate / total * 100) : 0,
|
|
6102
|
+
scopeAccuratePct: total > 0 ? Math.round(scopeAccurate / total * 100) : 0
|
|
6103
|
+
};
|
|
6104
|
+
});
|
|
6105
|
+
}
|
|
6074
6106
|
async getRecommendationEffectiveness() {
|
|
6075
6107
|
const rows = await this.sql`
|
|
6076
6108
|
SELECT type, total::text, actioned::text, dismissed::text, pending::text,
|
|
@@ -6387,6 +6419,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6387
6419
|
}
|
|
6388
6420
|
async searchDocs(input) {
|
|
6389
6421
|
const status = input.status ?? "active";
|
|
6422
|
+
const matchAllStatuses = status === "all";
|
|
6390
6423
|
const limit = input.limit ?? 10;
|
|
6391
6424
|
const keyword = input.keyword ? `%${input.keyword}%` : null;
|
|
6392
6425
|
const sinceCycle = input.sinceCycle ?? 0;
|
|
@@ -6394,7 +6427,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6394
6427
|
const rows = await this.sql`
|
|
6395
6428
|
SELECT * FROM doc_registry
|
|
6396
6429
|
WHERE project_id = ${this.projectId}
|
|
6397
|
-
AND status = ${status}
|
|
6430
|
+
AND (${matchAllStatuses} OR status = ${status})
|
|
6398
6431
|
AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
|
|
6399
6432
|
AND (${keyword}::text IS NULL OR (title ILIKE ${keyword} OR summary ILIKE ${keyword}))
|
|
6400
6433
|
AND (${sinceCycle} = 0 OR COALESCE(cycle_updated, cycle_created) >= ${sinceCycle})
|
|
@@ -6569,6 +6602,14 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6569
6602
|
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6570
6603
|
`;
|
|
6571
6604
|
}
|
|
6605
|
+
async confirmPendingActiveDecisions(cycleNumber) {
|
|
6606
|
+
await this.sql`
|
|
6607
|
+
UPDATE active_decisions
|
|
6608
|
+
SET outcome = 'confirmed'
|
|
6609
|
+
WHERE project_id = ${this.projectId} AND outcome = 'pending'
|
|
6610
|
+
`;
|
|
6611
|
+
void cycleNumber;
|
|
6612
|
+
}
|
|
6572
6613
|
// -------------------------------------------------------------------------
|
|
6573
6614
|
// Board (Tasks)
|
|
6574
6615
|
// -------------------------------------------------------------------------
|
|
@@ -6837,6 +6878,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6837
6878
|
createdAt: r.created_at ? r.created_at.toISOString() : void 0
|
|
6838
6879
|
}));
|
|
6839
6880
|
}
|
|
6881
|
+
async updateCycleLearningActionRef(learningId, taskDisplayId) {
|
|
6882
|
+
await this.sql`
|
|
6883
|
+
UPDATE cycle_learnings
|
|
6884
|
+
SET action_ref = ${taskDisplayId}
|
|
6885
|
+
WHERE id = ${learningId}
|
|
6886
|
+
AND project_id = ${this.projectId}
|
|
6887
|
+
AND action_ref IS NULL
|
|
6888
|
+
`;
|
|
6889
|
+
}
|
|
6840
6890
|
async getCycleLearningPatterns() {
|
|
6841
6891
|
const rows = await this.sql`
|
|
6842
6892
|
SELECT tag,
|
|
@@ -8626,6 +8676,28 @@ function formatBuildReports(reports) {
|
|
|
8626
8676
|
- **Dead Ends:** ${r.deadEnds}` : "")
|
|
8627
8677
|
).join("\n\n---\n\n");
|
|
8628
8678
|
}
|
|
8679
|
+
function formatRecentlyShippedCapabilities(reports) {
|
|
8680
|
+
const completed = reports.filter((r) => r.completed === "Yes" || r.completed === "Partial");
|
|
8681
|
+
if (completed.length === 0) return void 0;
|
|
8682
|
+
const lines = completed.map((r) => {
|
|
8683
|
+
const parts = [`- **${r.taskId}:** ${r.taskName}`];
|
|
8684
|
+
if (r.architectureNotes && r.architectureNotes !== "None") {
|
|
8685
|
+
const trimmed = r.architectureNotes.length > 150 ? r.architectureNotes.slice(0, 150) + "..." : r.architectureNotes;
|
|
8686
|
+
parts.push(` _Delivered:_ ${trimmed}`);
|
|
8687
|
+
}
|
|
8688
|
+
if (r.filesChanged && r.filesChanged.length > 0) {
|
|
8689
|
+
parts.push(` _Files:_ ${r.filesChanged.slice(0, 5).join(", ")}${r.filesChanged.length > 5 ? ` (+${r.filesChanged.length - 5} more)` : ""}`);
|
|
8690
|
+
}
|
|
8691
|
+
return parts.join("\n");
|
|
8692
|
+
});
|
|
8693
|
+
return [
|
|
8694
|
+
`${completed.length} task(s) completed in recent cycles:`,
|
|
8695
|
+
"",
|
|
8696
|
+
...lines,
|
|
8697
|
+
"",
|
|
8698
|
+
"Cross-reference candidate tasks against this list. If >80% of a candidate task's scope appears here, recommend cancellation or scope reduction instead of scheduling."
|
|
8699
|
+
].join("\n");
|
|
8700
|
+
}
|
|
8629
8701
|
function formatCycleLog(entries) {
|
|
8630
8702
|
if (entries.length === 0) return "No cycle log entries yet.";
|
|
8631
8703
|
return entries.map(
|
|
@@ -9279,6 +9351,39 @@ function resolveBaseBranch(cwd, preferred) {
|
|
|
9279
9351
|
if (preferred !== "master" && branchExists(cwd, "master")) return "master";
|
|
9280
9352
|
return preferred;
|
|
9281
9353
|
}
|
|
9354
|
+
function detectBoardMismatches(cwd, tasks) {
|
|
9355
|
+
const empty = { codeAhead: [], staleInProgress: [] };
|
|
9356
|
+
if (!isGitAvailable() || !isGitRepo(cwd)) return empty;
|
|
9357
|
+
try {
|
|
9358
|
+
const mergedOutput = execFileSync("git", ["branch", "--merged", "HEAD"], {
|
|
9359
|
+
cwd,
|
|
9360
|
+
encoding: "utf-8",
|
|
9361
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
9362
|
+
});
|
|
9363
|
+
const allOutput = execFileSync("git", ["branch"], {
|
|
9364
|
+
cwd,
|
|
9365
|
+
encoding: "utf-8",
|
|
9366
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
9367
|
+
});
|
|
9368
|
+
const parseBranches = (raw) => new Set(raw.split("\n").map((l) => l.trim().replace(/^\* /, "")).filter(Boolean));
|
|
9369
|
+
const mergedBranches = parseBranches(mergedOutput);
|
|
9370
|
+
const allBranches = parseBranches(allOutput);
|
|
9371
|
+
const codeAhead = [];
|
|
9372
|
+
const staleInProgress = [];
|
|
9373
|
+
for (const task of tasks) {
|
|
9374
|
+
const branch = `feat/${task.displayId}`;
|
|
9375
|
+
if (task.status === "Backlog" && mergedBranches.has(branch)) {
|
|
9376
|
+
codeAhead.push({ displayId: task.displayId, title: task.title, branch });
|
|
9377
|
+
}
|
|
9378
|
+
if (task.status === "In Progress" && !allBranches.has(branch)) {
|
|
9379
|
+
staleInProgress.push({ displayId: task.displayId, title: task.title });
|
|
9380
|
+
}
|
|
9381
|
+
}
|
|
9382
|
+
return { codeAhead, staleInProgress };
|
|
9383
|
+
} catch {
|
|
9384
|
+
return empty;
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9282
9387
|
function taskBranchName(taskId) {
|
|
9283
9388
|
return `feat/${taskId}`;
|
|
9284
9389
|
}
|
|
@@ -9621,13 +9726,19 @@ Standard planning cycle with full board review.
|
|
|
9621
9726
|
|
|
9622
9727
|
### Steps:
|
|
9623
9728
|
1. **Cycle Health Check** \u2014 Flag issues: >7 day gaps, unprocessed discovered issues, AD conflicts, stale In Progress tasks (3+ cycles).
|
|
9624
|
-
**\u26A0\uFE0F CARRY-FORWARD STALENESS:** Check the latest carry-forward text for items containing "stale", "already exists", "already implemented", or "already built". For each such item that references a specific task ID, check whether the task is still in Backlog. If a carry-forward says a task's deliverables already exist but the task is still Backlog, emit a \`boardCorrections\` entry setting it to Done with \`closureReason: "Auto-closed \u2014 carry-forward indicates deliverables already exist"\`. Log in the cycle log: "Auto-closed task-XXX \u2014 carry-forward confirmed deliverables exist." This prevents scheduling already-shipped tasks.
|
|
9729
|
+
**\u26A0\uFE0F CARRY-FORWARD STALENESS (already-built):** Check the latest carry-forward text for items containing "stale", "already exists", "already implemented", or "already built". For each such item that references a specific task ID, check whether the task is still in Backlog. If a carry-forward says a task's deliverables already exist but the task is still Backlog, emit a \`boardCorrections\` entry setting it to Done with \`closureReason: "Auto-closed \u2014 carry-forward indicates deliverables already exist"\`. Log in the cycle log: "Auto-closed task-XXX \u2014 carry-forward confirmed deliverables exist." This prevents scheduling already-shipped tasks.
|
|
9730
|
+
**\u26A0\uFE0F CARRY-FORWARD FORCED RESOLUTION:** If a "Carry-Forward Staleness" section is provided in the context below, it lists task IDs that have been deferred for 3+ consecutive cycles. For each listed task, you MUST take one of these actions \u2014 deferring again without justification is not acceptable:
|
|
9731
|
+
- **Escalate:** Emit a \`boardCorrections\` entry upgrading the task to P1 High (if currently P2+) and include a "Carry-Forward Escalation" paragraph in the cycle log: list each escalated task by ID with a 1-sentence rationale.
|
|
9732
|
+
- **Cancel:** Emit a \`boardCorrections\` entry setting status to "Cancelled" with a \`closureReason\` explaining why the task is no longer worth pursuing.
|
|
9733
|
+
- **Schedule:** Include the task in this cycle's BUILD HANDOFFs. If scheduled, no further action needed.
|
|
9734
|
+
If you defer a stale task again, you MUST provide an explicit justification in the cycle log \u2014 e.g. "task-XXX deferred again: blocked on task-YYY which must ship first."
|
|
9625
9735
|
|
|
9626
9736
|
2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You MUST set priority on unreviewed tasks during triage using these criteria:
|
|
9627
9737
|
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
|
|
9628
9738
|
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
|
|
9629
9739
|
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
|
|
9630
9740
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
9741
|
+
**\u26A0\uFE0F PRIORITY RECALIBRATION \u2014 do NOT rubber-stamp the submitted priority.** The priority set at idea submission reflects the submitter's view at that time, which may be outdated by the time the planner runs. For EVERY unreviewed task, evaluate its priority FROM SCRATCH against: (a) current horizon/stage/phase goals, (b) recent Active Decision changes, (c) recently shipped functionality that makes this task more or less urgent. If your assessed priority differs from the submitted one, set the new priority in \`boardCorrections\` and include the change in a **Priority Recalibration** paragraph in your cycle log (Step 8): list each changed task by ID, old priority \u2192 new priority, and a 1-sentence rationale. This paragraph is how the user sees what the planner recalibrated and why. If no priorities changed during triage, omit the paragraph.
|
|
9631
9742
|
Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
|
|
9632
9743
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
9633
9744
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
@@ -9635,6 +9746,7 @@ Standard planning cycle with full board review.
|
|
|
9635
9746
|
3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
|
|
9636
9747
|
**\u2192 PERSIST:** Include any field corrections (status updates, field fixes) in \`boardCorrections\` in Part 2.
|
|
9637
9748
|
**\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array). Priority values: P0 Critical, P1 High, P2 Medium, P3 Low.
|
|
9749
|
+
**Priority Drift Check:** For reviewed Backlog tasks, check whether their priority still reflects strategic reality. A task submitted as P2 six cycles ago may now be P1 (strategic context shifted) or P3 (redundant, superseded, or de-prioritised by an AD). For each task where drift is detected, check three signals: (1) Does it still align with the current horizon/stage/phase? (2) Has a recent AD changed the strategic importance of this area? (3) Has a recent build shipped functionality that makes this task redundant or more urgent? If drift is found on 1+ tasks, include a **Priority Drift Suggestions** paragraph in your cycle log: list each drifted task by ID, current priority, suggested priority, and a 1-sentence rationale. Do NOT include priority changes in \`boardCorrections\` \u2014 these are suggestions requiring human confirmation, not auto-corrections. Omit this paragraph entirely if no drift is detected.
|
|
9638
9750
|
|
|
9639
9751
|
4. **Security Posture Check** \u2014 Review recently completed tasks and current board state for security concerns. Only flag genuine issues \u2014 do not add boilerplate security notes every cycle. Look for:
|
|
9640
9752
|
- Data exposure risks introduced by recent builds (PII in logs, secrets in storage/config)
|
|
@@ -9652,7 +9764,8 @@ Standard planning cycle with full board review.
|
|
|
9652
9764
|
- **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas from the idea tool. The planner IS the scoping mechanism \u2014 scope them as part of planning. For raw tasks selected for a cycle: (a) derive clear scope, acceptance criteria, and effort from the title, notes, and project context, (b) upgrade them to \`maturity: "investigated"\` via a \`boardCorrections\` entry, and (c) generate a BUILD HANDOFF as normal. For research-type raw tasks, scope the handoff as an investigation task \u2014 the deliverable is findings + follow-up backlog tasks, not code. Only leave a raw task unscheduled if you genuinely cannot derive scope from the available context \u2014 note why in the cycle log. Tasks with \`maturity: "ready"\` or no maturity field are considered cycle-ready. Tasks with \`maturity: "investigated"\` have been scoped but may still need refinement \u2014 schedule them if priority warrants it.
|
|
9653
9765
|
- **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
|
|
9654
9766
|
|
|
9655
|
-
7. **Recommendation** \u2014
|
|
9767
|
+
7. **Recommendation** \u2014 Select tasks for this cycle:
|
|
9768
|
+
**Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots (up to 5 total) from the backlog using the priority rules below.
|
|
9656
9769
|
**If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
|
|
9657
9770
|
**Otherwise, select by priority level then impact:**
|
|
9658
9771
|
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
|
|
@@ -9664,7 +9777,7 @@ Standard planning cycle with full board review.
|
|
|
9664
9777
|
**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 from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 4-6 tasks. Cycles with fewer than 4 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 5+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 4 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.
|
|
9665
9778
|
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
9666
9779
|
|
|
9667
|
-
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates.
|
|
9780
|
+
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
9668
9781
|
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
9669
9782
|
|
|
9670
9783
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
@@ -9678,7 +9791,7 @@ Standard planning cycle with full board review.
|
|
|
9678
9791
|
|
|
9679
9792
|
10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
|
|
9680
9793
|
**SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
|
|
9681
|
-
**Scope pre-check:** Before writing the SCOPE section of each handoff,
|
|
9794
|
+
**Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
9682
9795
|
**Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
|
|
9683
9796
|
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
|
|
9684
9797
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
@@ -9714,7 +9827,7 @@ Standard planning cycle with full board review.
|
|
|
9714
9827
|
- **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
|
|
9715
9828
|
- **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
|
|
9716
9829
|
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
9717
|
-
- **Dogfood observations:** If unactioned dogfood entries are listed in context (with IDs), check if any map to existing tasks. If not, propose a new task. Include \`dogfood
|
|
9830
|
+
- **Dogfood observations:** If unactioned dogfood entries are listed in context (with IDs), check if any map to existing tasks. If not, propose a new task. **CRITICAL: Include \`dogfood:<uuid>\` in the new task's \`notes\` field** (e.g. \`"notes": "Addresses recurring friction. 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.
|
|
9718
9831
|
Create new tasks via the \`newTasks\` array in Part 2. Use \`new-N\` IDs in \`cycleHandoffs\` to reference them. **Limit: 3 new tasks per cycle** to prevent backlog bloat.
|
|
9719
9832
|
**\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.
|
|
9720
9833
|
**\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.
|
|
@@ -9734,6 +9847,151 @@ Standard planning cycle with full board review.
|
|
|
9734
9847
|
If the Forward Horizon context is absent or there are no meaningful decisions to surface, omit this section entirely. Do NOT generate generic advice like "plan ahead" or "consider testing".
|
|
9735
9848
|
|
|
9736
9849
|
**CRITICAL: Review your Part 2 JSON before finishing. Every action from Part 1 must have a corresponding entry in Part 2. If Part 1 mentions corrections, new tasks, AD changes, or handoffs but Part 2 has empty arrays \u2014 you have a persistence bug.**`;
|
|
9850
|
+
var PLAN_FRAGMENT_DISCOVERY_GAPS = `
|
|
9851
|
+
5. **Discovery Gaps** \u2014 If a Discovery Canvas section is provided in the context below, check which sections are populated vs empty. In cycles 1-10, or whenever canvas sections have been empty for 5+ cycles, include a "Discovery Gaps" paragraph in your cycle log suggesting what context would improve planning. Examples: "Your project context would benefit from MVP boundary definition" or "Consider documenting key user journeys." Keep suggestions conversational \u2014 do NOT create tasks for discovery gaps. If all canvas sections are populated, or no Discovery Canvas is provided, skip this step entirely.`;
|
|
9852
|
+
var PLAN_FRAGMENT_RESEARCH = `
|
|
9853
|
+
**Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
|
|
9854
|
+
|
|
9855
|
+
RESEARCH OUTPUT
|
|
9856
|
+
Deliverable: docs/research/[topic]-findings.md (draft path)
|
|
9857
|
+
Review status: pending owner approval
|
|
9858
|
+
Follow-up tasks: DO NOT submit to backlog until owner confirms findings are actionable
|
|
9859
|
+
|
|
9860
|
+
Also add to ACCEPTANCE CRITERIA: "[ ] Findings doc drafted and saved to docs/research/ before submitting any follow-up ideas"`;
|
|
9861
|
+
var PLAN_FRAGMENT_BUG = `
|
|
9862
|
+
**Bug task detection:** When a task's task type is "bug" or the title starts with "Bug:" or "Fix:", apply these rules:
|
|
9863
|
+
- **Auto-P1:** If the task's current priority is P2 or lower, upgrade it to "P1 High" via a boardCorrections entry in Part 2. Note the upgrade in Part 1 analysis.
|
|
9864
|
+
- Add a BLAST RADIUS note to the BUILD HANDOFF SCOPE section: "Bug fix \u2014 minimal blast radius. Change only what is necessary to fix the reported behaviour. Do not refactor surrounding code or expand scope."
|
|
9865
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Fix is targeted \u2014 no unrelated code changed"`;
|
|
9866
|
+
var PLAN_FRAGMENT_IDEA = `
|
|
9867
|
+
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
9868
|
+
- Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."`;
|
|
9869
|
+
var PLAN_FRAGMENT_UI = `
|
|
9870
|
+
**UI/visual task detection:** When a task's title or notes contain keywords suggesting frontend visual work (e.g. "visual", "design", "UI", "styling", "refresh", "frontend", "landing page", "hero", "carousel", "theme", "layout", "cockpit", "dashboard", "page"), apply these handoff additions:
|
|
9871
|
+
- Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
|
|
9872
|
+
- For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
|
|
9873
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
9874
|
+
- If the task involves image selection, add to SCOPE: "Include brand/theme direction constraints for image selection."
|
|
9875
|
+
The planner's job is scoping, not design direction. Design decisions happen at build time via \`.impeccable.md\` and the frontend-design skill \u2014 don't try to write design specs in the handoff.`;
|
|
9876
|
+
var PLAN_FRAGMENT_PRODUCT_BRIEF = `
|
|
9877
|
+
12. **Product Brief** \u2014 Check whether the product brief still reflects reality. Update the brief when ANY of these apply:
|
|
9878
|
+
- A new AD was created or an existing AD was superseded that changes product scope, target user, or positioning
|
|
9879
|
+
- The North Star changed or was validated in a way that the brief doesn't reflect
|
|
9880
|
+
- A phase completed that shifts what the product IS (not just what was built)
|
|
9881
|
+
- The brief describes capabilities, architecture, or direction that are no longer accurate
|
|
9882
|
+
- **DRIFT CHECK:** Compare the brief's content against current reality. The brief is drifted if: (a) it describes capabilities that don't exist or have been removed, (b) it references user types, architecture, or positioning that ADs have since changed, (c) the current phase/stage has shifted from what the brief describes, or (d) key metrics or success criteria no longer match the project's direction. Cycle count since last update is a secondary signal only \u2014 a brief updated 15 cycles ago that still accurately describes the product is NOT stale. A brief updated 3 cycles ago that contradicts a recent AD IS drifted.
|
|
9883
|
+
If any of these apply, include an updated \`productBrief\` in the structured output. Include the FULL updated brief (not a diff). Preserve all existing sections and user-added content; update facts, numbers, and status to reflect current reality. Do not regenerate the brief every cycle \u2014 but do not let it go stale either.`;
|
|
9884
|
+
var PLAN_FRAGMENT_FORWARD_HORIZON = `
|
|
9885
|
+
13. **Forward Horizon** \u2014 If a Forward Horizon section is provided in the context below, write a "## Forward Horizon" section in Part 1. Surface 2-3 decisions the team should make before the next phase starts. Each item must be:
|
|
9886
|
+
- **Specific** \u2014 reference the upcoming phase by name and the architectural fork or tradeoff involved
|
|
9887
|
+
- **Actionable** \u2014 frame as a decision to make, not a vague warning (e.g. "Decide whether to use WebSockets or SSE for real-time updates before starting Phase 4: Real-Time Features")
|
|
9888
|
+
- **Tied to trajectory** \u2014 based on current board state, ADs, and velocity, not generic advice
|
|
9889
|
+
If the Forward Horizon context is absent or there are no meaningful decisions to surface, omit this section entirely. Do NOT generate generic advice like "plan ahead" or "consider testing".`;
|
|
9890
|
+
function buildPlanFullInstructionsConditional(flags, ctx) {
|
|
9891
|
+
if (!flags || !ctx) return PLAN_FULL_INSTRUCTIONS;
|
|
9892
|
+
const parts = [
|
|
9893
|
+
`## FULL MODE
|
|
9894
|
+
|
|
9895
|
+
Standard planning cycle with full board review.
|
|
9896
|
+
|
|
9897
|
+
### Steps:
|
|
9898
|
+
1. **Cycle Health Check** \u2014 Flag issues: >7 day gaps, unprocessed discovered issues, AD conflicts, stale In Progress tasks (3+ cycles).
|
|
9899
|
+
**\u26A0\uFE0F CARRY-FORWARD STALENESS (already-built):** Check the latest carry-forward text for items containing "stale", "already exists", "already implemented", or "already built". For each such item that references a specific task ID, check whether the task is still in Backlog. If a carry-forward says a task's deliverables already exist but the task is still Backlog, emit a \`boardCorrections\` entry setting it to Done with \`closureReason: "Auto-closed \u2014 carry-forward indicates deliverables already exist"\`. Log in the cycle log: "Auto-closed task-XXX \u2014 carry-forward confirmed deliverables exist." This prevents scheduling already-shipped tasks.
|
|
9900
|
+
**\u26A0\uFE0F CARRY-FORWARD FORCED RESOLUTION:** If a "Carry-Forward Staleness" section is provided in the context below, it lists task IDs that have been deferred for 3+ consecutive cycles. For each listed task, you MUST take one of these actions \u2014 deferring again without justification is not acceptable:
|
|
9901
|
+
- **Escalate:** Emit a \`boardCorrections\` entry upgrading the task to P1 High (if currently P2+) and include a "Carry-Forward Escalation" paragraph in the cycle log: list each escalated task by ID with a 1-sentence rationale.
|
|
9902
|
+
- **Cancel:** Emit a \`boardCorrections\` entry setting status to "Cancelled" with a \`closureReason\` explaining why the task is no longer worth pursuing.
|
|
9903
|
+
- **Schedule:** Include the task in this cycle's BUILD HANDOFFs. If scheduled, no further action needed.
|
|
9904
|
+
If you defer a stale task again, you MUST provide an explicit justification in the cycle log \u2014 e.g. "task-XXX deferred again: blocked on task-YYY which must ship first."
|
|
9905
|
+
|
|
9906
|
+
2. **Inbox Triage** \u2014 Find unreviewed tasks (reviewed = false). For each: clean title, fill all fields, check for duplicates, verify alignment with Active Decisions. You MUST set priority on unreviewed tasks during triage using these criteria:
|
|
9907
|
+
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Fix now.
|
|
9908
|
+
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon/phase goals or Active Decisions.
|
|
9909
|
+
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infrastructure.
|
|
9910
|
+
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
9911
|
+
**\u26A0\uFE0F PRIORITY RECALIBRATION \u2014 do NOT rubber-stamp the submitted priority.** The priority set at idea submission reflects the submitter's view at that time, which may be outdated by the time the planner runs. For EVERY unreviewed task, evaluate its priority FROM SCRATCH against: (a) current horizon/stage/phase goals, (b) recent Active Decision changes, (c) recently shipped functionality that makes this task more or less urgent. If your assessed priority differs from the submitted one, set the new priority in \`boardCorrections\` and include the change in a **Priority Recalibration** paragraph in your cycle log (Step 8): list each changed task by ID, old priority \u2192 new priority, and a 1-sentence rationale. This paragraph is how the user sees what the planner recalibrated and why. If no priorities changed during triage, omit the paragraph.
|
|
9912
|
+
Also set complexity using the full range \u2014 **XS, Small, Medium, Large, XL** \u2014 based on actual scope, not conservatively. XS = single-line or config change. Small = one file, < 50 lines. Medium = 2-5 files. Large = cross-module, multiple components. XL = architectural, multi-day.
|
|
9913
|
+
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
9914
|
+
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
9915
|
+
|
|
9916
|
+
3. **Board Integrity** \u2014 All tasks have complete fields? Priority still accurate? Duplicates? Stale In Progress tasks?
|
|
9917
|
+
**\u2192 PERSIST:** Include any field corrections (status updates, field fixes) in \`boardCorrections\` in Part 2.
|
|
9918
|
+
**\u26A0\uFE0F PRIORITY LOCK RULE:** Do NOT change the priority of any task that has \`reviewed: true\`. Reviewed tasks have had their priority confirmed by a human. If you believe a reviewed task's priority should change, note your recommendation in the cycle log but do NOT include a priority change in \`boardCorrections\`. You may only set priority on unreviewed tasks (during triage) or on newly created tasks (\`newTasks\` array). Priority values: P0 Critical, P1 High, P2 Medium, P3 Low.
|
|
9919
|
+
**Priority Drift Check:** For reviewed Backlog tasks, check whether their priority still reflects strategic reality. A task submitted as P2 six cycles ago may now be P1 (strategic context shifted) or P3 (redundant, superseded, or de-prioritised by an AD). For each task where drift is detected, check three signals: (1) Does it still align with the current horizon/stage/phase? (2) Has a recent AD changed the strategic importance of this area? (3) Has a recent build shipped functionality that makes this task redundant or more urgent? If drift is found on 1+ tasks, include a **Priority Drift Suggestions** paragraph in your cycle log: list each drifted task by ID, current priority, suggested priority, and a 1-sentence rationale. Do NOT include priority changes in \`boardCorrections\` \u2014 these are suggestions requiring human confirmation, not auto-corrections. Omit this paragraph entirely if no drift is detected.
|
|
9920
|
+
|
|
9921
|
+
4. **Security Posture Check** \u2014 Review recently completed tasks and current board state for security concerns. Only flag genuine issues \u2014 do not add boilerplate security notes every cycle. Look for:
|
|
9922
|
+
- Data exposure risks introduced by recent builds (PII in logs, secrets in storage/config)
|
|
9923
|
+
- Unprotected endpoints or missing auth/access control in new features
|
|
9924
|
+
- Undocumented secrets or environment variables added without documentation
|
|
9925
|
+
- New dependencies with known vulnerabilities or excessive permissions
|
|
9926
|
+
**\u2192 PERSIST:** If concerns exist, include them in \`cycleLogNotes\` with a \`[SECURITY]\` tag prefix (e.g. "[SECURITY] New /admin endpoint in task-042 has no auth middleware"). If no concerns, omit \u2014 do not write "[SECURITY] No issues found".`
|
|
9927
|
+
];
|
|
9928
|
+
if (ctx.hasDiscoveryCanvas) {
|
|
9929
|
+
parts.push(PLAN_FRAGMENT_DISCOVERY_GAPS);
|
|
9930
|
+
}
|
|
9931
|
+
parts.push(`
|
|
9932
|
+
6. **Maturity Gate** \u2014 Before scheduling any task, check whether the project is ready for it:
|
|
9933
|
+
- **Cycle number as signal:** A Cycle 3 project should not be scheduling OAuth, billing, or analytics tasks. Early cycles focus on core functionality and proving the concept works.
|
|
9934
|
+
- **Phase prerequisites:** If the board has phases, tasks from later phases should only be scheduled when earlier phases have completed tasks (check Done count per phase). A task in "Phase 4: Monetisation" is premature if Phase 2 tasks are still in Backlog.
|
|
9935
|
+
- **Dependency chain:** If a task's \`dependsOn\` references incomplete tasks, it cannot be scheduled regardless of priority.
|
|
9936
|
+
- **Task maturity:** Tasks with \`maturity: "raw"\` are unscoped ideas from the idea tool. The planner IS the scoping mechanism \u2014 scope them as part of planning. For raw tasks selected for a cycle: (a) derive clear scope, acceptance criteria, and effort from the title, notes, and project context, (b) upgrade them to \`maturity: "investigated"\` via a \`boardCorrections\` entry, and (c) generate a BUILD HANDOFF as normal. For research-type raw tasks, scope the handoff as an investigation task \u2014 the deliverable is findings + follow-up backlog tasks, not code. Only leave a raw task unscheduled if you genuinely cannot derive scope from the available context \u2014 note why in the cycle log. Tasks with \`maturity: "ready"\` or no maturity field are considered cycle-ready. Tasks with \`maturity: "investigated"\` have been scoped but may still need refinement \u2014 schedule them if priority warrants it.
|
|
9937
|
+
- **What to do with premature tasks:** Leave them in Backlog. Do NOT generate BUILD HANDOFFs for them. If a high-priority task fails the maturity gate due to phase prerequisites or dependencies, note it in the cycle log: "task-XXX deferred \u2014 Phase N prerequisites not met". Raw tasks are NOT premature \u2014 they just need scoping (see Task maturity above).
|
|
9938
|
+
|
|
9939
|
+
7. **Recommendation** \u2014 Select tasks for this cycle:
|
|
9940
|
+
**Pre-assigned tasks:** If a "Pre-Assigned Tasks" section is provided in the context below, those tasks are ALREADY committed to this cycle by the user. Include them automatically \u2014 do NOT re-evaluate whether they belong. Generate BUILD HANDOFFs for each. Count their effort toward the cycle budget. Then fill remaining slots (up to 5 total) from the backlog using the priority rules below.
|
|
9941
|
+
**If USER DIRECTION is provided above:** Follow the user's stated focus. Pick the highest-impact task that aligns with their direction. The user knows what they need. Only deviate if a genuine P0 Critical fix exists (broken builds, data loss).
|
|
9942
|
+
**Otherwise, select by priority level then impact:**
|
|
9943
|
+
- **P0 Critical** \u2014 Broken, blocking, or data-loss risk. Always first.
|
|
9944
|
+
- **P1 High** \u2014 Strategically aligned: directly advances the current horizon, phase, or Active Decision goals.
|
|
9945
|
+
- **P2 Medium** \u2014 Valuable but not strategically urgent: quality improvements, efficiency, polish, infra.
|
|
9946
|
+
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
9947
|
+
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.
|
|
9948
|
+
**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.
|
|
9949
|
+
**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 from the tasks themselves. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 4-6 tasks. Cycles with fewer than 4 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 5+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 4 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.
|
|
9950
|
+
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
9951
|
+
|
|
9952
|
+
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
9953
|
+
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
9954
|
+
|
|
9955
|
+
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
9956
|
+
**AD Quality Bar:** ADs are for product and architecture choices that constrain future work \u2014 technology selections, data model designs, UX principles, strategic positioning. They are NOT for: process preferences (commit style, PR size), configuration choices (linter rules, tab width), or temporary workarounds. If a decision doesn't affect what gets built or how it's architected, it's not an AD. Apply this bar when proposing new ADs and when triaging existing ones.
|
|
9957
|
+
**\u2192 PERSIST:** EVERY AD you created, updated, or confirmed with changes MUST appear in \`activeDecisions\` array in Part 2. Include the full replacement body with ### heading.
|
|
9958
|
+
|
|
9959
|
+
### Operational Quality Rules
|
|
9960
|
+
- **Idea similarity pause:** When the idea tool finds similar tasks during planning, stop and explain the overlap \u2014 do not silently ignore the similarity warning. Duplicates bloat the board and waste build slots.
|
|
9961
|
+
- **Backlog as steering wheel:** Task priority and notes in the backlog are the user's primary control mechanism over what gets planned. Respect the priority rankings and read task notes carefully \u2014 they contain user intent that shapes scope and scheduling.
|
|
9962
|
+
- **Planning quality is the bar:** Strategy review depth and plan quality set the standard for the product. Do not cut corners on analysis depth, triage thoroughness, or handoff specificity \u2014 these are what users experience as PAPI's value.
|
|
9963
|
+
|
|
9964
|
+
10. **BUILD HANDOFFs** \u2014 Generate a full BUILD HANDOFF block for the recommended task and up to 4 additional high-priority unblocked tasks (5 total max). Include each handoff in the \`cycleHandoffs\` array in the structured output. The handoffs are written to each task on the board for durability. Remaining tasks will get handoffs in subsequent plans \u2014 do NOT try to cover the entire backlog.
|
|
9965
|
+
**SKIP existing handoffs:** Tasks marked with "Has BUILD HANDOFF: yes" or "\u2713 handoff" on the board already have a valid handoff from a previous plan. Do NOT regenerate handoffs for these tasks \u2014 omit them from the \`cycleHandoffs\` array entirely. Only generate handoffs for tasks that do NOT have one yet. Exception: if a task's dependencies have been completed since its handoff was written, or a relevant Active Decision has changed, you MAY regenerate its handoff \u2014 but note this explicitly in the cycle log.
|
|
9966
|
+
**Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
9967
|
+
**Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
|
|
9968
|
+
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
|
|
9969
|
+
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
9970
|
+
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
9971
|
+
**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.
|
|
9972
|
+
**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.`);
|
|
9973
|
+
if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
|
|
9974
|
+
if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
|
|
9975
|
+
if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
|
|
9976
|
+
if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
|
|
9977
|
+
parts.push(`
|
|
9978
|
+
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
9979
|
+
- **Discovered Issues:** If a build report lists a discovered issue and no existing board task covers it, propose a new task.
|
|
9980
|
+
- **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
|
|
9981
|
+
- **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
|
|
9982
|
+
- **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
|
|
9983
|
+
- **Dogfood observations:** If unactioned dogfood entries are listed in context (with IDs), check if any map to existing tasks. If not, propose a new task. **CRITICAL: Include \`dogfood:<uuid>\` in the new task's \`notes\` field** (e.g. \`"notes": "Addresses recurring friction. 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.
|
|
9984
|
+
Create new tasks via the \`newTasks\` array in Part 2. Use \`new-N\` IDs in \`cycleHandoffs\` to reference them. **Limit: 3 new tasks per cycle** to prevent backlog bloat.
|
|
9985
|
+
**\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.
|
|
9986
|
+
**\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.`);
|
|
9987
|
+
parts.push(PLAN_FRAGMENT_PRODUCT_BRIEF);
|
|
9988
|
+
if (ctx.hasHorizonContext) {
|
|
9989
|
+
parts.push(PLAN_FRAGMENT_FORWARD_HORIZON);
|
|
9990
|
+
}
|
|
9991
|
+
parts.push(`
|
|
9992
|
+
**CRITICAL: Review your Part 2 JSON before finishing. Every action from Part 1 must have a corresponding entry in Part 2. If Part 1 mentions corrections, new tasks, AD changes, or handoffs but Part 2 has empty arrays \u2014 you have a persistence bug.**`);
|
|
9993
|
+
return parts.join("\n");
|
|
9994
|
+
}
|
|
9737
9995
|
function buildPlanUserMessage(ctx) {
|
|
9738
9996
|
const modeLabel = ctx.mode.toUpperCase();
|
|
9739
9997
|
const parts = [
|
|
@@ -9754,7 +10012,11 @@ function buildPlanUserMessage(ctx) {
|
|
|
9754
10012
|
if (ctx.mode === "bootstrap") {
|
|
9755
10013
|
parts.push(PLAN_BOOTSTRAP_INSTRUCTIONS);
|
|
9756
10014
|
} else {
|
|
9757
|
-
|
|
10015
|
+
const instructions = ctx.boardFlags ? buildPlanFullInstructionsConditional(ctx.boardFlags, {
|
|
10016
|
+
hasDiscoveryCanvas: !!ctx.discoveryCanvas,
|
|
10017
|
+
hasHorizonContext: !!ctx.horizonContext
|
|
10018
|
+
}) : PLAN_FULL_INSTRUCTIONS;
|
|
10019
|
+
parts.push(instructions);
|
|
9758
10020
|
}
|
|
9759
10021
|
parts.push("", "---", "", "## PROJECT CONTEXT", "");
|
|
9760
10022
|
parts.push("### Product Brief", "", ctx.productBrief, "");
|
|
@@ -9768,12 +10030,18 @@ function buildPlanUserMessage(ctx) {
|
|
|
9768
10030
|
if (ctx.recentBuildReports) {
|
|
9769
10031
|
parts.push("### Recent Build Reports", "", ctx.recentBuildReports, "");
|
|
9770
10032
|
}
|
|
10033
|
+
if (ctx.recentlyShippedCapabilities) {
|
|
10034
|
+
parts.push("### Recently Shipped Capabilities", "", ctx.recentlyShippedCapabilities, "");
|
|
10035
|
+
}
|
|
9771
10036
|
if (ctx.cycleLog) {
|
|
9772
10037
|
parts.push("### Cycle Log", "", ctx.cycleLog, "");
|
|
9773
10038
|
}
|
|
9774
10039
|
if (ctx.board) {
|
|
9775
10040
|
parts.push("### Board", "", ctx.board, "");
|
|
9776
10041
|
}
|
|
10042
|
+
if (ctx.preAssignedTasks) {
|
|
10043
|
+
parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
|
|
10044
|
+
}
|
|
9777
10045
|
if (ctx.buildPatterns) {
|
|
9778
10046
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
9779
10047
|
}
|
|
@@ -9804,6 +10072,71 @@ function buildPlanUserMessage(ctx) {
|
|
|
9804
10072
|
if (ctx.discoveryCanvas) {
|
|
9805
10073
|
parts.push("### Discovery Canvas", "", ctx.discoveryCanvas, "");
|
|
9806
10074
|
}
|
|
10075
|
+
if (ctx.registeredDocs) {
|
|
10076
|
+
parts.push("### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
10077
|
+
}
|
|
10078
|
+
if (ctx.carryForwardStaleness) {
|
|
10079
|
+
parts.push("### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
10080
|
+
}
|
|
10081
|
+
}
|
|
10082
|
+
return parts.join("\n");
|
|
10083
|
+
}
|
|
10084
|
+
function buildHandoffsOnlyUserMessage(inputs) {
|
|
10085
|
+
const targetCycle = inputs.cycleNumber + 1;
|
|
10086
|
+
const taskLines = inputs.preAssignedTasks.map((t) => {
|
|
10087
|
+
const notes = t.notes ? `
|
|
10088
|
+
Notes: ${t.notes.slice(0, 300)}` : "";
|
|
10089
|
+
const hasHandoff = t.hasHandoff;
|
|
10090
|
+
return `- **${t.id}:** ${t.title}
|
|
10091
|
+
Priority: ${t.priority} | Complexity: ${t.complexity} | Module: ${t.module}
|
|
10092
|
+
Phase: ${t.phase} | Epic: ${t.epic}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${notes}`;
|
|
10093
|
+
});
|
|
10094
|
+
const parts = [
|
|
10095
|
+
`## MODE: HANDOFFS-ONLY`,
|
|
10096
|
+
`## Cycle Number: ${targetCycle}`,
|
|
10097
|
+
"",
|
|
10098
|
+
`## HANDOFFS-ONLY MODE`,
|
|
10099
|
+
"",
|
|
10100
|
+
`The user has pre-assigned ${inputs.preAssignedTasks.length} task(s) to Cycle ${targetCycle} and wants BUILD HANDOFFs generated without full backlog analysis.`,
|
|
10101
|
+
"",
|
|
10102
|
+
`**Skip these steps entirely:** Inbox Triage, Board Integrity, Security Posture Check, Discovery Gaps, Maturity Gate, Recommendation (task selection), New Tasks, Product Brief update.`,
|
|
10103
|
+
"",
|
|
10104
|
+
`**Do these steps only:**`,
|
|
10105
|
+
`1. Generate a BUILD HANDOFF for each pre-assigned task listed below.`,
|
|
10106
|
+
`2. Write a brief Cycle Log entry (3-5 lines) noting this was a handoffs-only cycle and listing the tasks.`,
|
|
10107
|
+
`3. If any task already has a BUILD HANDOFF (marked "Has BUILD HANDOFF: yes"), skip it \u2014 do NOT regenerate.`,
|
|
10108
|
+
"",
|
|
10109
|
+
`Follow the BUILD HANDOFF format from the system prompt. Include SCOPE, SCOPE BOUNDARY, ACCEPTANCE CRITERIA, SECURITY CONSIDERATIONS, PRE-BUILD VERIFICATION, FILES LIKELY TOUCHED, and EFFORT for each task.`,
|
|
10110
|
+
"",
|
|
10111
|
+
`Your output MUST have TWO parts:`,
|
|
10112
|
+
`1. Natural language markdown with BUILD HANDOFF blocks`,
|
|
10113
|
+
`2. After \`<!-- PAPI_STRUCTURED_OUTPUT -->\`, a JSON block with structured data`,
|
|
10114
|
+
"",
|
|
10115
|
+
`The structured output format is the same as full mode. Use empty arrays for \`newTasks\`, \`boardCorrections\`, and \`activeDecisions\`. Populate \`cycleHandoffs\` with your handoffs.`,
|
|
10116
|
+
"",
|
|
10117
|
+
`---`,
|
|
10118
|
+
"",
|
|
10119
|
+
`## PRE-ASSIGNED TASKS`,
|
|
10120
|
+
"",
|
|
10121
|
+
...taskLines,
|
|
10122
|
+
"",
|
|
10123
|
+
`---`,
|
|
10124
|
+
"",
|
|
10125
|
+
`## PROJECT CONTEXT`,
|
|
10126
|
+
"",
|
|
10127
|
+
`### Product Brief`,
|
|
10128
|
+
"",
|
|
10129
|
+
inputs.productBrief,
|
|
10130
|
+
""
|
|
10131
|
+
];
|
|
10132
|
+
if (inputs.northStar) {
|
|
10133
|
+
parts.push("### North Star (current)", "", inputs.northStar, "");
|
|
10134
|
+
}
|
|
10135
|
+
if (inputs.activeDecisions) {
|
|
10136
|
+
parts.push("### Active Decisions", "", inputs.activeDecisions, "");
|
|
10137
|
+
}
|
|
10138
|
+
if (inputs.recentBuildReports) {
|
|
10139
|
+
parts.push("### Recent Build Reports", "", inputs.recentBuildReports, "");
|
|
9807
10140
|
}
|
|
9808
10141
|
return parts.join("\n");
|
|
9809
10142
|
}
|
|
@@ -10612,6 +10945,45 @@ function buildPlanSlackSummary(cycleNumber, mode, data) {
|
|
|
10612
10945
|
}
|
|
10613
10946
|
return parts.join("\n");
|
|
10614
10947
|
}
|
|
10948
|
+
function computeCarryForwardStaleness(log) {
|
|
10949
|
+
if (log.length === 0) return void 0;
|
|
10950
|
+
const sorted = [...log].sort((a, b2) => a.cycleNumber - b2.cycleNumber);
|
|
10951
|
+
const taskCounts = /* @__PURE__ */ new Map();
|
|
10952
|
+
for (const entry of sorted) {
|
|
10953
|
+
if (!entry.carryForward) continue;
|
|
10954
|
+
const matches = entry.carryForward.match(/task-\d+/g) ?? [];
|
|
10955
|
+
const seen = new Set(matches);
|
|
10956
|
+
for (const id of seen) {
|
|
10957
|
+
taskCounts.set(id, (taskCounts.get(id) ?? 0) + 1);
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
const stale = [...taskCounts.entries()].filter(([, count]) => count >= 3);
|
|
10961
|
+
if (stale.length === 0) return void 0;
|
|
10962
|
+
const lines = stale.map(([id, count]) => `- **${id}** \u2014 deferred ${count} consecutive cycle(s)`);
|
|
10963
|
+
return [
|
|
10964
|
+
`\u26A0\uFE0F ${stale.length} task(s) have been in carry-forward for 3+ consecutive cycles. The planner must resolve each \u2014 either escalate to P1 High or recommend cancellation with a closure reason. Deferring again without justification is not acceptable.`,
|
|
10965
|
+
"",
|
|
10966
|
+
...lines
|
|
10967
|
+
].join("\n");
|
|
10968
|
+
}
|
|
10969
|
+
function formatPreAssignedTasks(tasks, targetCycle) {
|
|
10970
|
+
if (tasks.length === 0) return void 0;
|
|
10971
|
+
const lines = tasks.map((t) => {
|
|
10972
|
+
const notes = t.notes ? ` \u2014 ${t.notes.slice(0, 200)}` : "";
|
|
10973
|
+
return `- **${t.id}:** ${t.title} [${t.priority} | ${t.complexity} | ${t.module}]${notes}`;
|
|
10974
|
+
});
|
|
10975
|
+
const effort = tasks.reduce((sum, t) => {
|
|
10976
|
+
const map = { XS: 1, S: 2, Small: 2, M: 3, Medium: 3, L: 5, Large: 5, XL: 8 };
|
|
10977
|
+
return sum + (map[t.complexity] ?? 3);
|
|
10978
|
+
}, 0);
|
|
10979
|
+
return [
|
|
10980
|
+
`${tasks.length} task(s) pre-assigned to Cycle ${targetCycle} (~${effort} effort points):`,
|
|
10981
|
+
"",
|
|
10982
|
+
...lines,
|
|
10983
|
+
"",
|
|
10984
|
+
"These tasks MUST be included in the cycle. Generate BUILD HANDOFFs for each. Fill remaining slots (up to 5 total) from the backlog."
|
|
10985
|
+
].join("\n");
|
|
10986
|
+
}
|
|
10615
10987
|
function pushAfterCommit(config2) {
|
|
10616
10988
|
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
10617
10989
|
return "";
|
|
@@ -10771,6 +11143,28 @@ function stripTasksForPlan(tasks) {
|
|
|
10771
11143
|
hasHandoff: !!buildHandoff
|
|
10772
11144
|
}));
|
|
10773
11145
|
}
|
|
11146
|
+
function detectBoardFlags(tasks) {
|
|
11147
|
+
let hasBugTasks = false;
|
|
11148
|
+
let hasResearchTasks = false;
|
|
11149
|
+
let hasIdeaTasks = false;
|
|
11150
|
+
let hasUITasks = false;
|
|
11151
|
+
const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
|
|
11152
|
+
for (const t of tasks) {
|
|
11153
|
+
if (t.taskType === "bug" || /^(Bug:|Fix:)/i.test(t.title)) hasBugTasks = true;
|
|
11154
|
+
if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
|
|
11155
|
+
if (t.taskType === "idea") hasIdeaTasks = true;
|
|
11156
|
+
if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
|
|
11157
|
+
}
|
|
11158
|
+
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasUITasks };
|
|
11159
|
+
}
|
|
11160
|
+
function detectBoardFlagsFromText(boardText) {
|
|
11161
|
+
return {
|
|
11162
|
+
hasBugTasks: /\b(bug|Bug:|Fix:)\b/i.test(boardText),
|
|
11163
|
+
hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
|
|
11164
|
+
hasIdeaTasks: /\bidea\b/i.test(boardText),
|
|
11165
|
+
hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
|
|
11166
|
+
};
|
|
11167
|
+
}
|
|
10774
11168
|
function hashSection(content) {
|
|
10775
11169
|
return createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
10776
11170
|
}
|
|
@@ -10780,7 +11174,13 @@ function applyContextDiff(ctx, prevHashes) {
|
|
|
10780
11174
|
const sections = [
|
|
10781
11175
|
{ key: "productBrief", hashKey: "productBrief" },
|
|
10782
11176
|
{ key: "activeDecisions", hashKey: "activeDecisions" },
|
|
10783
|
-
{ key: "recentReviews", hashKey: "reviews" }
|
|
11177
|
+
{ key: "recentReviews", hashKey: "reviews" },
|
|
11178
|
+
{ key: "board", hashKey: "board" },
|
|
11179
|
+
{ key: "recentBuildReports", hashKey: "buildReports" },
|
|
11180
|
+
{ key: "cycleLog", hashKey: "cycleLog" },
|
|
11181
|
+
{ key: "discoveryCanvas", hashKey: "discoveryCanvas" },
|
|
11182
|
+
{ key: "estimationCalibration", hashKey: "estimationCalibration" },
|
|
11183
|
+
{ key: "methodologyMetrics", hashKey: "methodologyMetrics" }
|
|
10784
11184
|
];
|
|
10785
11185
|
for (const { key, hashKey } of sections) {
|
|
10786
11186
|
const content = ctx[key];
|
|
@@ -10914,10 +11314,54 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10914
11314
|
const discoveryCanvasText = await assembleDiscoveryCanvasText(adapter2);
|
|
10915
11315
|
let estimationCalibrationText;
|
|
10916
11316
|
try {
|
|
10917
|
-
const calibrationRows = await
|
|
11317
|
+
const [calibrationRows, moduleStats] = await Promise.all([
|
|
11318
|
+
adapter2.getEstimationCalibration?.(),
|
|
11319
|
+
adapter2.getModuleEstimationStats?.()
|
|
11320
|
+
]);
|
|
10918
11321
|
if (calibrationRows && calibrationRows.length > 0) {
|
|
10919
11322
|
estimationCalibrationText = formatEstimationCalibration(calibrationRows);
|
|
10920
11323
|
}
|
|
11324
|
+
if (moduleStats && moduleStats.length > 0) {
|
|
11325
|
+
const moduleLines = [
|
|
11326
|
+
"",
|
|
11327
|
+
"**By module (last 20 cycles, min 3 builds):**",
|
|
11328
|
+
"| Module | Builds | Effort match | Scope accurate |",
|
|
11329
|
+
"|--------|--------|--------------|----------------|",
|
|
11330
|
+
...moduleStats.map((r) => `| ${r.module} | ${r.total} | ${r.accuracyPct}% | ${r.scopeAccuratePct}% |`)
|
|
11331
|
+
].join("\n");
|
|
11332
|
+
estimationCalibrationText = (estimationCalibrationText ?? "") + moduleLines;
|
|
11333
|
+
}
|
|
11334
|
+
} catch {
|
|
11335
|
+
}
|
|
11336
|
+
let registeredDocsText;
|
|
11337
|
+
try {
|
|
11338
|
+
const docs = await adapter2.searchDocs?.({ status: "active", limit: 5 });
|
|
11339
|
+
if (docs && docs.length > 0) {
|
|
11340
|
+
const lines = docs.map((d) => `- **${d.title}** (${d.type}) \u2014 ${d.summary}`);
|
|
11341
|
+
registeredDocsText = `${docs.length} active research doc(s):
|
|
11342
|
+
${lines.join("\n")}`;
|
|
11343
|
+
}
|
|
11344
|
+
} catch {
|
|
11345
|
+
}
|
|
11346
|
+
const boardFlags = detectBoardFlagsFromText(leanSummary.board);
|
|
11347
|
+
let carryForwardStalenessLean;
|
|
11348
|
+
try {
|
|
11349
|
+
const recentLog = await adapter2.getCycleLog(5);
|
|
11350
|
+
carryForwardStalenessLean = computeCarryForwardStaleness(recentLog);
|
|
11351
|
+
} catch {
|
|
11352
|
+
}
|
|
11353
|
+
const leanTargetCycle = health.totalCycles + 1;
|
|
11354
|
+
let preAssignedTextLean;
|
|
11355
|
+
try {
|
|
11356
|
+
const preAssignedTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] });
|
|
11357
|
+
const preAssigned2 = preAssignedTasks.filter((t2) => t2.cycle === leanTargetCycle);
|
|
11358
|
+
preAssignedTextLean = formatPreAssignedTasks(preAssigned2, leanTargetCycle);
|
|
11359
|
+
} catch {
|
|
11360
|
+
}
|
|
11361
|
+
let recentlyShippedLean;
|
|
11362
|
+
try {
|
|
11363
|
+
const reportsForCaps = await adapter2.getRecentBuildReports(10);
|
|
11364
|
+
recentlyShippedLean = formatRecentlyShippedCapabilities(reportsForCaps);
|
|
10921
11365
|
} catch {
|
|
10922
11366
|
}
|
|
10923
11367
|
let ctx2 = {
|
|
@@ -10936,7 +11380,12 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
10936
11380
|
taskComments: taskCommentsText,
|
|
10937
11381
|
discoveryCanvas: discoveryCanvasText,
|
|
10938
11382
|
estimationCalibration: estimationCalibrationText,
|
|
10939
|
-
|
|
11383
|
+
registeredDocs: registeredDocsText,
|
|
11384
|
+
focus,
|
|
11385
|
+
boardFlags,
|
|
11386
|
+
carryForwardStaleness: carryForwardStalenessLean,
|
|
11387
|
+
preAssignedTasks: preAssignedTextLean,
|
|
11388
|
+
recentlyShippedCapabilities: recentlyShippedLean
|
|
10940
11389
|
};
|
|
10941
11390
|
t = startTimer();
|
|
10942
11391
|
const prevHashes2 = await adapter2.getContextHashes?.(health.totalCycles) ?? null;
|
|
@@ -11001,25 +11450,53 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
11001
11450
|
console.error(`[plan-perf] assembleContext (full): ${JSON.stringify(timings)}ms`);
|
|
11002
11451
|
const discoveryCanvasTextFull = await assembleDiscoveryCanvasText(adapter2);
|
|
11003
11452
|
const taskCommentsTextFull = await assembleTaskComments(adapter2);
|
|
11453
|
+
let registeredDocsTextFull;
|
|
11454
|
+
try {
|
|
11455
|
+
const docs = await adapter2.searchDocs?.({ status: "active", limit: 5 });
|
|
11456
|
+
if (docs && docs.length > 0) {
|
|
11457
|
+
const lines = docs.map((d) => `- **${d.title}** (${d.type}) \u2014 ${d.summary}`);
|
|
11458
|
+
registeredDocsTextFull = `${docs.length} active research doc(s):
|
|
11459
|
+
${lines.join("\n")}`;
|
|
11460
|
+
}
|
|
11461
|
+
} catch {
|
|
11462
|
+
}
|
|
11463
|
+
const MAX_PLAN_BUILD_REPORT_CYCLES = 3;
|
|
11464
|
+
const cappedReports = reports.length > 0 ? (() => {
|
|
11465
|
+
const maxCycle = Math.max(...reports.map((r) => r.cycle ?? 0));
|
|
11466
|
+
const cutoff = maxCycle - MAX_PLAN_BUILD_REPORT_CYCLES + 1;
|
|
11467
|
+
const filtered = reports.filter((r) => (r.cycle ?? 0) >= cutoff);
|
|
11468
|
+
return filtered.length > 0 ? filtered : reports.slice(0, 5);
|
|
11469
|
+
})() : reports;
|
|
11470
|
+
const strippedTasks = stripTasksForPlan(tasks);
|
|
11471
|
+
const boardFlagsFull = detectBoardFlags(tasks);
|
|
11472
|
+
const horizonCtx = buildHorizonContext(phases, tasks) ?? void 0;
|
|
11473
|
+
const targetCycle = health.totalCycles + 1;
|
|
11474
|
+
const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
|
|
11475
|
+
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
11004
11476
|
let ctx = {
|
|
11005
11477
|
mode,
|
|
11006
11478
|
cycleNumber: health.totalCycles,
|
|
11007
11479
|
productBrief,
|
|
11008
11480
|
activeDecisions: formatActiveDecisionsForPlan(decisions),
|
|
11009
|
-
recentBuildReports: formatBuildReports(
|
|
11481
|
+
recentBuildReports: formatBuildReports(cappedReports),
|
|
11010
11482
|
cycleLog: formatCycleLog(log),
|
|
11011
|
-
board: formatBoardForPlan(
|
|
11483
|
+
board: formatBoardForPlan(strippedTasks, filters, health.totalCycles),
|
|
11012
11484
|
northStar,
|
|
11013
11485
|
methodologyMetrics: formatCycleMetrics(metricsSnapshots),
|
|
11014
11486
|
recentReviews: formatReviews(reviews),
|
|
11015
11487
|
buildPatterns: buildPatternsText,
|
|
11016
11488
|
reviewPatterns: reviewPatternsText,
|
|
11017
|
-
horizonContext:
|
|
11489
|
+
horizonContext: horizonCtx,
|
|
11018
11490
|
strategyRecommendations: strategyRecommendationsText,
|
|
11019
11491
|
dogfoodEntries,
|
|
11020
11492
|
taskComments: taskCommentsTextFull,
|
|
11021
11493
|
discoveryCanvas: discoveryCanvasTextFull,
|
|
11022
|
-
|
|
11494
|
+
registeredDocs: registeredDocsTextFull,
|
|
11495
|
+
focus,
|
|
11496
|
+
boardFlags: boardFlagsFull,
|
|
11497
|
+
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
11498
|
+
preAssignedTasks: preAssignedText,
|
|
11499
|
+
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(allReportsForPatterns)
|
|
11023
11500
|
};
|
|
11024
11501
|
const prevHashes = await adapter2.getContextHashes?.(health.totalCycles) ?? null;
|
|
11025
11502
|
const { ctx: diffedCtx, newHashes, savedBytes } = applyContextDiff(ctx, prevHashes);
|
|
@@ -11236,12 +11713,45 @@ ${cleanContent}`;
|
|
|
11236
11713
|
notes: task.notes || ""
|
|
11237
11714
|
});
|
|
11238
11715
|
newTaskIdMap.set(`new-${i + 1}`, created.id);
|
|
11239
|
-
if (adapter2.
|
|
11240
|
-
const
|
|
11241
|
-
if (
|
|
11242
|
-
for (const ref of
|
|
11243
|
-
const
|
|
11244
|
-
|
|
11716
|
+
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
11717
|
+
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
11718
|
+
if (learningRefs) {
|
|
11719
|
+
for (const ref of learningRefs) {
|
|
11720
|
+
const learningId = ref.split(":")[1];
|
|
11721
|
+
try {
|
|
11722
|
+
await adapter2.updateCycleLearningActionRef(learningId, created.id);
|
|
11723
|
+
} catch {
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
}
|
|
11727
|
+
}
|
|
11728
|
+
if (adapter2.updateDogfoodEntryStatus) {
|
|
11729
|
+
let linked = false;
|
|
11730
|
+
if (task.notes) {
|
|
11731
|
+
const dogfoodRefs = task.notes.match(/dogfood:([a-f0-9-]+)/gi);
|
|
11732
|
+
if (dogfoodRefs) {
|
|
11733
|
+
for (const ref of dogfoodRefs) {
|
|
11734
|
+
const entryId = ref.split(":")[1];
|
|
11735
|
+
await adapter2.updateDogfoodEntryStatus(entryId, "backlog-created", created.id);
|
|
11736
|
+
}
|
|
11737
|
+
linked = true;
|
|
11738
|
+
}
|
|
11739
|
+
}
|
|
11740
|
+
if (!linked && adapter2.getUnactionedDogfoodEntries) {
|
|
11741
|
+
try {
|
|
11742
|
+
const unactioned = await adapter2.getUnactionedDogfoodEntries(50);
|
|
11743
|
+
if (unactioned.length > 0) {
|
|
11744
|
+
const taskText = normalise(`${task.title} ${task.notes || ""} ${task.why || ""}`);
|
|
11745
|
+
const taskWords = new Set(taskText.split(/\s+/).filter((w) => w.length >= 4));
|
|
11746
|
+
for (const entry of unactioned) {
|
|
11747
|
+
const entryWords = normalise(entry.content).split(/\s+/).filter((w) => w.length >= 4);
|
|
11748
|
+
const overlap = entryWords.filter((w) => taskWords.has(w)).length;
|
|
11749
|
+
if (overlap >= 3 && entry.id) {
|
|
11750
|
+
await adapter2.updateDogfoodEntryStatus(entry.id, "backlog-created", created.id);
|
|
11751
|
+
}
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
} catch {
|
|
11245
11755
|
}
|
|
11246
11756
|
}
|
|
11247
11757
|
}
|
|
@@ -11537,11 +12047,49 @@ async function processLlmOutput(adapter2, config2, rawOutput, mode, cycleNumber,
|
|
|
11537
12047
|
writeSummary
|
|
11538
12048
|
};
|
|
11539
12049
|
}
|
|
11540
|
-
async function preparePlan(adapter2, config2, filters, focus, force) {
|
|
12050
|
+
async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnly) {
|
|
11541
12051
|
const prepareTimer = startTimer();
|
|
11542
12052
|
let t = startTimer();
|
|
11543
12053
|
const { mode, cycleNumber, strategyReviewWarning } = await validateAndPrepare(adapter2, force);
|
|
11544
12054
|
const validateMs = t();
|
|
12055
|
+
if (handoffsOnly) {
|
|
12056
|
+
t = startTimer();
|
|
12057
|
+
const targetCycle = cycleNumber + 1;
|
|
12058
|
+
const allTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready", "In Progress"] });
|
|
12059
|
+
const preAssigned = allTasks.filter((task) => task.cycle === targetCycle);
|
|
12060
|
+
if (preAssigned.length === 0) {
|
|
12061
|
+
throw new Error(`No tasks assigned to Cycle ${targetCycle}. Assign tasks first (set cycle = ${targetCycle} via SQL) or run plan without handoffs_only.`);
|
|
12062
|
+
}
|
|
12063
|
+
const [decisions, reports, brief] = await Promise.all([
|
|
12064
|
+
adapter2.getActiveDecisions(),
|
|
12065
|
+
adapter2.getRecentBuildReports(10),
|
|
12066
|
+
adapter2.getProductBrief()
|
|
12067
|
+
]);
|
|
12068
|
+
const northStar = await adapter2.getCurrentNorthStar?.() ?? "";
|
|
12069
|
+
const userMessage2 = buildHandoffsOnlyUserMessage({
|
|
12070
|
+
cycleNumber,
|
|
12071
|
+
preAssignedTasks: preAssigned,
|
|
12072
|
+
activeDecisions: formatActiveDecisionsForPlan(decisions),
|
|
12073
|
+
recentBuildReports: formatBuildReports(reports),
|
|
12074
|
+
productBrief: brief,
|
|
12075
|
+
northStar
|
|
12076
|
+
});
|
|
12077
|
+
const assembleMs2 = t();
|
|
12078
|
+
const totalMs2 = prepareTimer();
|
|
12079
|
+
console.error(`[plan-perf] preparePlan (handoffs-only): validate=${validateMs}ms assemble=${assembleMs2}ms total=${totalMs2}ms`);
|
|
12080
|
+
const contextBytes2 = Buffer.byteLength(userMessage2, "utf-8");
|
|
12081
|
+
console.error(`[plan-perf] contextBytes=${contextBytes2} (handoffs-only)`);
|
|
12082
|
+
const planSystemPrompt2 = await getPrompt("plan-system");
|
|
12083
|
+
return {
|
|
12084
|
+
mode: "full",
|
|
12085
|
+
// apply phase treats it the same
|
|
12086
|
+
cycleNumber,
|
|
12087
|
+
systemPrompt: planSystemPrompt2,
|
|
12088
|
+
userMessage: userMessage2,
|
|
12089
|
+
strategyReviewWarning,
|
|
12090
|
+
contextBytes: contextBytes2
|
|
12091
|
+
};
|
|
12092
|
+
}
|
|
11545
12093
|
t = startTimer();
|
|
11546
12094
|
const { context, contextHashes } = await assembleContext(adapter2, mode, config2, filters, focus);
|
|
11547
12095
|
const assembleMs = t();
|
|
@@ -11723,7 +12271,7 @@ var lastPrepareUserMessage;
|
|
|
11723
12271
|
var lastPrepareContextBytes;
|
|
11724
12272
|
var planTool = {
|
|
11725
12273
|
name: "plan",
|
|
11726
|
-
description: 'Run once per cycle to generate BUILD HANDOFFs for up to
|
|
12274
|
+
description: 'Run once per cycle to generate BUILD HANDOFFs for up to 5 tasks. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results.',
|
|
11727
12275
|
inputSchema: {
|
|
11728
12276
|
type: "object",
|
|
11729
12277
|
properties: {
|
|
@@ -11772,6 +12320,10 @@ var planTool = {
|
|
|
11772
12320
|
force: {
|
|
11773
12321
|
type: "boolean",
|
|
11774
12322
|
description: "Bypass planning guards (unreleased cycle block, strategy review hard-block). Only use when explicitly requested by the user."
|
|
12323
|
+
},
|
|
12324
|
+
handoffs_only: {
|
|
12325
|
+
type: "boolean",
|
|
12326
|
+
description: "Skip backlog analysis and task selection. Only generate BUILD HANDOFFs for tasks already assigned to the target cycle. Requires pre-assigned tasks (set cycle number on tasks first). ~30% of normal plan cost."
|
|
11775
12327
|
}
|
|
11776
12328
|
},
|
|
11777
12329
|
required: []
|
|
@@ -11811,6 +12363,10 @@ function formatPlanResult(result) {
|
|
|
11811
12363
|
if (result.slackWarning) lines.push(result.slackWarning);
|
|
11812
12364
|
if (result.autoCommitNote) lines.push(result.autoCommitNote.trim());
|
|
11813
12365
|
lines.push("", `Next: run \`build_list\` to see your cycle tasks, then \`build_execute <task_id>\` to start building.`);
|
|
12366
|
+
if (result.contextBytes !== void 0) {
|
|
12367
|
+
const kb = (result.contextBytes / 1024).toFixed(1);
|
|
12368
|
+
lines.push(`---`, `Context: ${kb}KB`);
|
|
12369
|
+
}
|
|
11814
12370
|
const response = textResponse(lines.join("\n"));
|
|
11815
12371
|
if (result.contextUtilisation !== void 0) {
|
|
11816
12372
|
return { ...response, _contextUtilisation: result.contextUtilisation };
|
|
@@ -11826,6 +12382,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
11826
12382
|
if (typeof args.priority === "string") filters.priority = args.priority;
|
|
11827
12383
|
const focus = typeof args.focus === "string" ? args.focus : void 0;
|
|
11828
12384
|
const force = args.force === true;
|
|
12385
|
+
const handoffsOnly = args.handoffs_only === true;
|
|
11829
12386
|
try {
|
|
11830
12387
|
if (toolMode === "apply") {
|
|
11831
12388
|
const llmResponse = args.llm_response;
|
|
@@ -11849,7 +12406,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
11849
12406
|
} catch {
|
|
11850
12407
|
}
|
|
11851
12408
|
}
|
|
11852
|
-
const response = formatPlanResult({ ...result, contextUtilisation: utilisation });
|
|
12409
|
+
const response = formatPlanResult({ ...result, contextUtilisation: utilisation, contextBytes });
|
|
11853
12410
|
return {
|
|
11854
12411
|
...response,
|
|
11855
12412
|
...contextBytes !== void 0 ? { _contextBytes: contextBytes } : {},
|
|
@@ -11861,7 +12418,7 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
11861
12418
|
await propagatePhaseStatus(adapter2);
|
|
11862
12419
|
} catch {
|
|
11863
12420
|
}
|
|
11864
|
-
const result = await preparePlan(adapter2, config2, filters, focus, force);
|
|
12421
|
+
const result = await preparePlan(adapter2, config2, filters, focus, force, handoffsOnly);
|
|
11865
12422
|
lastPrepareContextHashes = result.contextHashes;
|
|
11866
12423
|
lastPrepareUserMessage = result.userMessage;
|
|
11867
12424
|
lastPrepareContextBytes = result.contextBytes;
|
|
@@ -12008,7 +12565,9 @@ function extractRecommendations(data, cycleNumber) {
|
|
|
12008
12565
|
}
|
|
12009
12566
|
}
|
|
12010
12567
|
if (!data.actionItems?.length && data.strategicRecommendations) {
|
|
12011
|
-
const
|
|
12568
|
+
const recsValue = data.strategicRecommendations;
|
|
12569
|
+
const recsStr = Array.isArray(recsValue) ? recsValue.join("\n") : String(recsValue);
|
|
12570
|
+
const lines = recsStr.split("\n");
|
|
12012
12571
|
for (const line of lines) {
|
|
12013
12572
|
const bullet = line.replace(/^[\s*-]+/, "").trim();
|
|
12014
12573
|
if (!bullet) continue;
|
|
@@ -12605,6 +13164,12 @@ ${cleanContent}`;
|
|
|
12605
13164
|
}
|
|
12606
13165
|
}));
|
|
12607
13166
|
}
|
|
13167
|
+
try {
|
|
13168
|
+
if (adapter2.confirmPendingActiveDecisions) {
|
|
13169
|
+
await adapter2.confirmPendingActiveDecisions(cycleNumber);
|
|
13170
|
+
}
|
|
13171
|
+
} catch {
|
|
13172
|
+
}
|
|
12608
13173
|
if (data.decisionScores && data.decisionScores.length > 0) {
|
|
12609
13174
|
await Promise.all(data.decisionScores.map(async (score) => {
|
|
12610
13175
|
try {
|
|
@@ -12663,6 +13228,17 @@ ${cleanContent}`;
|
|
|
12663
13228
|
} catch {
|
|
12664
13229
|
}
|
|
12665
13230
|
}
|
|
13231
|
+
if (adapter2.getUnactionedDogfoodEntries && adapter2.updateDogfoodEntryStatus) {
|
|
13232
|
+
try {
|
|
13233
|
+
const staleThreshold = cycleNumber - 10;
|
|
13234
|
+
const unactioned = await adapter2.getUnactionedDogfoodEntries(100);
|
|
13235
|
+
const stale = unactioned.filter((e) => e.cycleNumber <= staleThreshold);
|
|
13236
|
+
await Promise.all(
|
|
13237
|
+
stale.map((e) => adapter2.updateDogfoodEntryStatus(e.id, "dismissed"))
|
|
13238
|
+
);
|
|
13239
|
+
} catch {
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
12666
13242
|
try {
|
|
12667
13243
|
const canvas = await adapter2.readDiscoveryCanvas();
|
|
12668
13244
|
const updates = {};
|
|
@@ -13213,7 +13789,7 @@ var lastReviewUserMessage;
|
|
|
13213
13789
|
var lastReviewContextBytes;
|
|
13214
13790
|
var strategyReviewTool = {
|
|
13215
13791
|
name: "strategy_review",
|
|
13216
|
-
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions.
|
|
13792
|
+
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run in its own dedicated session \u2014 do not mix with building. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
|
|
13217
13793
|
inputSchema: {
|
|
13218
13794
|
type: "object",
|
|
13219
13795
|
properties: {
|
|
@@ -14055,6 +14631,21 @@ When updating a doc, add or update a review header immediately below the title:
|
|
|
14055
14631
|
|
|
14056
14632
|
Replace \`task-NNN\` with the task ID that triggered the update, and \`DD-MM-YYYY\` with today's date.
|
|
14057
14633
|
|
|
14634
|
+
## Session Start
|
|
14635
|
+
|
|
14636
|
+
When a conversation starts \u2014 fresh window, new session, or after context compression \u2014 orient before doing anything else:
|
|
14637
|
+
|
|
14638
|
+
1. **Run \`orient\`** \u2014 single call that returns cycle number, task counts, in-progress/in-review tasks, strategy review cadence, trends, and recommended next action.
|
|
14639
|
+
2. **Fix orphaned tasks silently** \u2014 check for feat/task-XXX branches that don't match board status. Fix and report after.
|
|
14640
|
+
3. **Summarise:** "You're on Cycle N. X tasks to build, Y builds pending review." or "Cycle N is complete \u2014 ready for the next plan."
|
|
14641
|
+
4. **Run \`build_list\` when picking a task** \u2014 \`orient\` shows counts only. \`build_list\` shows the full task list with handoffs.
|
|
14642
|
+
|
|
14643
|
+
**CRITICAL: Check task statuses before acting.**
|
|
14644
|
+
- **In Review** = already built. Suggest \`review_list\` \u2192 \`review_submit\`. **NEVER re-build an In Review task.**
|
|
14645
|
+
- **In Progress** = build started but not completed. Check the branch and existing changes before writing new code.
|
|
14646
|
+
- **Backlog** = not started. But first check if a \`feat/task-XXX\` branch already exists with commits \u2014 fix it, don't rebuild.
|
|
14647
|
+
- If all cycle tasks are Done, suggest \`release\` or next \`plan\`.
|
|
14648
|
+
|
|
14058
14649
|
## Workflow Sequences
|
|
14059
14650
|
|
|
14060
14651
|
PAPI tools follow structured flows. The agent manages the cycle workflow automatically \u2014 the user should never need to type tool names or remember the flow. Handle the plumbing, surface the summaries.
|
|
@@ -14100,6 +14691,15 @@ strategy_review \u2192 strategy_change
|
|
|
14100
14691
|
Next: \`strategy_change\` if the review recommends adjustments.
|
|
14101
14692
|
- **strategy_change** \u2014 Updates active decisions, north star, or project direction based on review findings.
|
|
14102
14693
|
|
|
14694
|
+
### Detect Strategic Decisions in Conversation
|
|
14695
|
+
|
|
14696
|
+
Watch for: direction changes, architecture shifts, deprioritisation with reasoning, new principles, competitive positioning decisions.
|
|
14697
|
+
|
|
14698
|
+
When detected:
|
|
14699
|
+
1. Flag it: "That sounds like a strategic direction change \u2014 should I run \`strategy_change\`?"
|
|
14700
|
+
2. If confirmed, run \`strategy_change\` immediately.
|
|
14701
|
+
3. If mid-build, finish the current task first.
|
|
14702
|
+
|
|
14103
14703
|
### Idea Capture
|
|
14104
14704
|
|
|
14105
14705
|
\`\`\`
|
|
@@ -14199,6 +14799,7 @@ When the system compresses prior messages, immediately:
|
|
|
14199
14799
|
- **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
|
|
14200
14800
|
- **M/L tasks or different modules:** Own branch per task. Isolated PRs.
|
|
14201
14801
|
- **Commit per task within grouped branches** \u2014 traceable git history.
|
|
14802
|
+
- **Never use \`build_execute\` with \`light=true\` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed \u2014 any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
|
|
14202
14803
|
|
|
14203
14804
|
## Quick Work vs PAPI Work
|
|
14204
14805
|
|
|
@@ -14211,6 +14812,9 @@ PAPI is for planned work. Quick fixes \u2014 just do them. No need for plan or b
|
|
|
14211
14812
|
- **Use MCP tools for all project data operations.** DB is the source of truth when using the pg adapter.
|
|
14212
14813
|
- Do NOT read \`.papi/\` files for context \u2014 use MCP tools.
|
|
14213
14814
|
- \`.papi/\` files may be stale when using pg adapter. This is expected.
|
|
14815
|
+
- **\`board_edit\` never updates the \`cycle\` field.** When moving a task into or out of a cycle, always run a SQL update alongside \`board_edit\`:
|
|
14816
|
+
- Adding to current cycle: \`UPDATE cycle_tasks SET cycle = <N> WHERE display_id = '<task-id>';\`
|
|
14817
|
+
- Removing from cycle (backlog): \`UPDATE cycle_tasks SET cycle = null WHERE display_id = '<task-id>';\`
|
|
14214
14818
|
|
|
14215
14819
|
## Code Before Claims \u2014 No Assumptions
|
|
14216
14820
|
|
|
@@ -14244,6 +14848,9 @@ These rules come from 80+ cycles of dogfooding. They prevent the most common sou
|
|
|
14244
14848
|
- **Telemetry opt-out.** PAPI collects anonymous usage data (tool name, duration, project ID). To disable, add \`"PAPI_TELEMETRY": "off"\` to the \`env\` block in your \`.mcp.json\`.
|
|
14245
14849
|
|
|
14246
14850
|
### Planning & Scope
|
|
14851
|
+
- **NEVER run \`plan\` more than once per cycle.** Adjust the cycle with \`board_deprioritise\` or \`idea\` instead.
|
|
14852
|
+
- **NEVER skip cycles.** Complete and release the current cycle before running the next \`plan\`.
|
|
14853
|
+
- **Only build tasks assigned to the current cycle.** Use \`build_list\` \u2014 it filters to current-cycle tasks with handoffs.
|
|
14247
14854
|
- **Don't ask premature questions.** If the project is in early cycles, don't ask about deployment accounts, hosting providers, OAuth setup, or commercial features. Focus on building core functionality first.
|
|
14248
14855
|
- **Split large ideas.** If an idea has 3+ concerns, submit it as 2-3 separate ideas so the planner creates properly scoped tasks \u2014 not kitchen-sink handoffs.
|
|
14249
14856
|
- **Auto-release completed cycles.** When all cycle tasks are Done and reviews accepted, run \`release\` immediately. Forgetting causes cycle number drift and merge conflicts in the next session.
|
|
@@ -14475,6 +15082,17 @@ async function scaffoldPapiDir(adapter2, config2, input) {
|
|
|
14475
15082
|
await writeFile2(join3(config2.papiDir, filename), content, "utf-8");
|
|
14476
15083
|
}
|
|
14477
15084
|
}
|
|
15085
|
+
} else {
|
|
15086
|
+
try {
|
|
15087
|
+
const existingBrief = await adapter2.readProductBrief();
|
|
15088
|
+
if (!existingBrief.trim()) {
|
|
15089
|
+
const templateContent = substitute(PRODUCT_BRIEF_TEMPLATE, vars);
|
|
15090
|
+
await adapter2.updateProductBrief(templateContent);
|
|
15091
|
+
} else {
|
|
15092
|
+
return false;
|
|
15093
|
+
}
|
|
15094
|
+
} catch {
|
|
15095
|
+
}
|
|
14478
15096
|
}
|
|
14479
15097
|
const commandsDir = join3(config2.projectRoot, ".claude", "commands");
|
|
14480
15098
|
const docsDir = join3(config2.projectRoot, "docs");
|
|
@@ -14786,7 +15404,7 @@ async function prepareSetup(adapter2, config2, input) {
|
|
|
14786
15404
|
);
|
|
14787
15405
|
}
|
|
14788
15406
|
const TEMPLATE_MARKER = "*Describe your project's core value proposition here.*";
|
|
14789
|
-
if (!existingBrief.includes(TEMPLATE_MARKER) && !input.force) {
|
|
15407
|
+
if (existingBrief.trim() && !existingBrief.includes(TEMPLATE_MARKER) && !input.force) {
|
|
14790
15408
|
throw new Error("PRODUCT_BRIEF.md already contains a generated Product Brief. Running setup again would overwrite it.\n\nTo proceed anyway, run setup with force: true.");
|
|
14791
15409
|
}
|
|
14792
15410
|
let codebaseSummary;
|
|
@@ -15200,7 +15818,7 @@ init_dist2();
|
|
|
15200
15818
|
|
|
15201
15819
|
// src/services/build.ts
|
|
15202
15820
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
15203
|
-
import { readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
15821
|
+
import { readdirSync as readdirSync2, existsSync as existsSync2, readFileSync } from "fs";
|
|
15204
15822
|
import { join as join4 } from "path";
|
|
15205
15823
|
function capitalizeCompleted(value) {
|
|
15206
15824
|
const map = {
|
|
@@ -15414,6 +16032,34 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
15414
16032
|
}
|
|
15415
16033
|
return { task, branchLines, phaseChanges };
|
|
15416
16034
|
}
|
|
16035
|
+
var VALID_DOC_TYPES = /* @__PURE__ */ new Set(["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"]);
|
|
16036
|
+
function extractDocMeta(absolutePath, relativePath, cycleNumber) {
|
|
16037
|
+
let title = relativePath.split("/").pop()?.replace(".md", "") ?? relativePath;
|
|
16038
|
+
let type = "reference";
|
|
16039
|
+
let cycle = cycleNumber;
|
|
16040
|
+
let summary = "Auto-registered \u2014 no summary available. Update via doc_register.";
|
|
16041
|
+
if (relativePath.startsWith("docs/research/")) type = "research";
|
|
16042
|
+
else if (relativePath.startsWith("docs/architecture/")) type = "architecture";
|
|
16043
|
+
else if (relativePath.startsWith("docs/audits/")) type = "audit";
|
|
16044
|
+
try {
|
|
16045
|
+
const content = readFileSync(absolutePath, "utf-8").slice(0, 2e3);
|
|
16046
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
16047
|
+
if (fmMatch) {
|
|
16048
|
+
const fm = fmMatch[1];
|
|
16049
|
+
const titleMatch = fm.match(/^title:\s*["']?(.+?)["']?\s*$/m);
|
|
16050
|
+
if (titleMatch) title = titleMatch[1].trim();
|
|
16051
|
+
const typeMatch = fm.match(/^type:\s*(\S+)/m);
|
|
16052
|
+
if (typeMatch && VALID_DOC_TYPES.has(typeMatch[1])) type = typeMatch[1];
|
|
16053
|
+
const cycleMatch = fm.match(/^cycle:\s*(\d+)/m);
|
|
16054
|
+
if (cycleMatch) cycle = parseInt(cycleMatch[1], 10);
|
|
16055
|
+
} else {
|
|
16056
|
+
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
16057
|
+
if (headingMatch) title = headingMatch[1].trim();
|
|
16058
|
+
}
|
|
16059
|
+
} catch {
|
|
16060
|
+
}
|
|
16061
|
+
return { title, type, cycle, summary, tags: [] };
|
|
16062
|
+
}
|
|
15417
16063
|
async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
15418
16064
|
const task = await adapter2.getTask(taskId);
|
|
15419
16065
|
if (!task) {
|
|
@@ -15497,6 +16143,18 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
15497
16143
|
}
|
|
15498
16144
|
}
|
|
15499
16145
|
}
|
|
16146
|
+
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
16147
|
+
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
16148
|
+
if (learningRefs) {
|
|
16149
|
+
for (const ref of learningRefs) {
|
|
16150
|
+
const learningId = ref.split(":")[1];
|
|
16151
|
+
try {
|
|
16152
|
+
await adapter2.updateCycleLearningActionRef(learningId, task.id);
|
|
16153
|
+
} catch {
|
|
16154
|
+
}
|
|
16155
|
+
}
|
|
16156
|
+
}
|
|
16157
|
+
}
|
|
15500
16158
|
if (report.relatedDecisions && report.relatedDecisions.length > 0) {
|
|
15501
16159
|
for (const adId of report.relatedDecisions) {
|
|
15502
16160
|
try {
|
|
@@ -15564,21 +16222,51 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
15564
16222
|
if (adapter2.searchDocs) {
|
|
15565
16223
|
const docsDir = join4(config2.projectRoot, "docs");
|
|
15566
16224
|
if (existsSync2(docsDir)) {
|
|
15567
|
-
const scanDir = (dir) => {
|
|
16225
|
+
const scanDir = (dir, depth = 0) => {
|
|
16226
|
+
if (depth > 8) return [];
|
|
15568
16227
|
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
15569
16228
|
const files = [];
|
|
15570
16229
|
for (const e of entries) {
|
|
15571
16230
|
const full = join4(dir, e.name);
|
|
15572
|
-
if (e.isDirectory()) files.push(...scanDir(full));
|
|
16231
|
+
if (e.isDirectory() && !e.isSymbolicLink()) files.push(...scanDir(full, depth + 1));
|
|
15573
16232
|
else if (e.name.endsWith(".md")) files.push(full.replace(config2.projectRoot + "/", ""));
|
|
15574
16233
|
}
|
|
15575
16234
|
return files;
|
|
15576
16235
|
};
|
|
15577
16236
|
const mdFiles = scanDir(docsDir);
|
|
15578
|
-
const registered = await adapter2.searchDocs({ status: "
|
|
16237
|
+
const registered = await adapter2.searchDocs({ status: "all", limit: 500 });
|
|
15579
16238
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
15580
16239
|
const unregistered = mdFiles.filter((f) => !registeredPaths.has(f));
|
|
15581
|
-
if (unregistered.length > 0) {
|
|
16240
|
+
if (unregistered.length > 0 && adapter2.registerDoc) {
|
|
16241
|
+
const autoRegistered = [];
|
|
16242
|
+
const failed = [];
|
|
16243
|
+
for (const docPath of unregistered) {
|
|
16244
|
+
try {
|
|
16245
|
+
const meta = extractDocMeta(join4(config2.projectRoot, docPath), docPath, cycleNumber);
|
|
16246
|
+
await adapter2.registerDoc({
|
|
16247
|
+
title: meta.title,
|
|
16248
|
+
type: meta.type,
|
|
16249
|
+
path: docPath,
|
|
16250
|
+
status: "active",
|
|
16251
|
+
summary: meta.summary,
|
|
16252
|
+
tags: meta.tags,
|
|
16253
|
+
cycleCreated: meta.cycle,
|
|
16254
|
+
cycleUpdated: meta.cycle
|
|
16255
|
+
});
|
|
16256
|
+
autoRegistered.push(docPath);
|
|
16257
|
+
} catch {
|
|
16258
|
+
failed.push(docPath);
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
const parts = [];
|
|
16262
|
+
if (autoRegistered.length > 0) {
|
|
16263
|
+
parts.push(`\u{1F4C4} Auto-registered ${autoRegistered.length} doc(s): ${autoRegistered.slice(0, 3).join(", ")}${autoRegistered.length > 3 ? ` (+${autoRegistered.length - 3} more)` : ""}`);
|
|
16264
|
+
}
|
|
16265
|
+
if (failed.length > 0) {
|
|
16266
|
+
parts.push(`${failed.length} doc(s) failed auto-registration \u2014 run doc_register manually: ${failed.slice(0, 3).join(", ")}`);
|
|
16267
|
+
}
|
|
16268
|
+
docWarning = parts.join(" | ") || void 0;
|
|
16269
|
+
} else if (unregistered.length > 0) {
|
|
15582
16270
|
docWarning = `${unregistered.length} unregistered doc(s) in docs/ \u2014 consider running \`doc_register\` for: ${unregistered.slice(0, 5).join(", ")}${unregistered.length > 5 ? ` (+${unregistered.length - 5} more)` : ""}`;
|
|
15583
16271
|
}
|
|
15584
16272
|
}
|
|
@@ -15712,7 +16400,7 @@ var buildDescribeTool = {
|
|
|
15712
16400
|
};
|
|
15713
16401
|
var buildExecuteTool = {
|
|
15714
16402
|
name: "build_execute",
|
|
15715
|
-
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
16403
|
+
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
15716
16404
|
inputSchema: {
|
|
15717
16405
|
type: "object",
|
|
15718
16406
|
properties: {
|
|
@@ -16374,10 +17062,25 @@ ${lines.join("\n")}
|
|
|
16374
17062
|
const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
16375
17063
|
const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
|
|
16376
17064
|
const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
|
|
17065
|
+
const PREFIX_MAP = {
|
|
17066
|
+
bug: "bug",
|
|
17067
|
+
research: "research",
|
|
17068
|
+
feedback: "feedback"
|
|
17069
|
+
};
|
|
17070
|
+
let taskTitle = input.text;
|
|
17071
|
+
let taskType = "idea";
|
|
17072
|
+
const prefixMatch = input.text.match(/^\[([a-zA-Z]+)\]\s*/);
|
|
17073
|
+
if (prefixMatch) {
|
|
17074
|
+
const key = prefixMatch[1].toLowerCase();
|
|
17075
|
+
if (key in PREFIX_MAP) {
|
|
17076
|
+
taskType = PREFIX_MAP[key];
|
|
17077
|
+
taskTitle = input.text.slice(prefixMatch[0].length);
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
16377
17080
|
const task = await adapter2.createTask({
|
|
16378
17081
|
uuid: randomUUID10(),
|
|
16379
17082
|
displayId: "",
|
|
16380
|
-
title:
|
|
17083
|
+
title: taskTitle,
|
|
16381
17084
|
status: "Backlog",
|
|
16382
17085
|
priority,
|
|
16383
17086
|
complexity,
|
|
@@ -16388,10 +17091,22 @@ ${lines.join("\n")}
|
|
|
16388
17091
|
reviewed: false,
|
|
16389
17092
|
createdCycle: health.totalCycles,
|
|
16390
17093
|
notes: input.notes || "",
|
|
16391
|
-
taskType
|
|
17094
|
+
taskType,
|
|
16392
17095
|
maturity: "raw",
|
|
16393
17096
|
docRef: input.docRef
|
|
16394
17097
|
});
|
|
17098
|
+
if (input.notes && adapter2.updateCycleLearningActionRef) {
|
|
17099
|
+
const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
17100
|
+
if (learningRefs) {
|
|
17101
|
+
for (const ref of learningRefs) {
|
|
17102
|
+
const learningId = ref.split(":")[1];
|
|
17103
|
+
try {
|
|
17104
|
+
await adapter2.updateCycleLearningActionRef(learningId, task.id);
|
|
17105
|
+
} catch {
|
|
17106
|
+
}
|
|
17107
|
+
}
|
|
17108
|
+
}
|
|
17109
|
+
}
|
|
16395
17110
|
if (input.notes && adapter2.updateDogfoodEntryStatus) {
|
|
16396
17111
|
const dogfoodRefs = input.notes.match(/dogfood:([a-f0-9-]+)/gi);
|
|
16397
17112
|
if (dogfoodRefs) {
|
|
@@ -17234,13 +17949,37 @@ async function handleBoardReconcile(adapter2, config2, args) {
|
|
|
17234
17949
|
if (context === "No backlog tasks to reconcile.") {
|
|
17235
17950
|
return textResponse(context);
|
|
17236
17951
|
}
|
|
17952
|
+
let mismatchSection = "";
|
|
17953
|
+
try {
|
|
17954
|
+
const allTasks = await adapter2.queryBoard();
|
|
17955
|
+
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
17956
|
+
if (mismatches.codeAhead.length > 0 || mismatches.staleInProgress.length > 0) {
|
|
17957
|
+
const lines = ["### Code-Ahead-of-Status Mismatches", ""];
|
|
17958
|
+
if (mismatches.codeAhead.length > 0) {
|
|
17959
|
+
lines.push("**Branch merged but task still Backlog** \u2014 these tasks likely built outside PAPI tracking:");
|
|
17960
|
+
for (const m of mismatches.codeAhead) {
|
|
17961
|
+
lines.push(`- **${m.displayId}**: ${m.title} (branch \`${m.branch}\` merged to main)`);
|
|
17962
|
+
}
|
|
17963
|
+
lines.push("");
|
|
17964
|
+
}
|
|
17965
|
+
if (mismatches.staleInProgress.length > 0) {
|
|
17966
|
+
lines.push("**In Progress but no feature branch** \u2014 may be stale or built without build_execute:");
|
|
17967
|
+
for (const m of mismatches.staleInProgress) {
|
|
17968
|
+
lines.push(`- **${m.displayId}**: ${m.title}`);
|
|
17969
|
+
}
|
|
17970
|
+
lines.push("");
|
|
17971
|
+
}
|
|
17972
|
+
mismatchSection = lines.join("\n") + "\n";
|
|
17973
|
+
}
|
|
17974
|
+
} catch {
|
|
17975
|
+
}
|
|
17237
17976
|
return textResponse(
|
|
17238
17977
|
`${RECONCILE_PROMPT}
|
|
17239
17978
|
---
|
|
17240
17979
|
|
|
17241
17980
|
### Backlog Context
|
|
17242
17981
|
|
|
17243
|
-
${context}
|
|
17982
|
+
${mismatchSection}${context}
|
|
17244
17983
|
---
|
|
17245
17984
|
|
|
17246
17985
|
Analyze the backlog above and produce your reconciliation output. Then call \`board_reconcile\` with mode "apply".`
|
|
@@ -17978,7 +18717,7 @@ var reviewListTool = {
|
|
|
17978
18717
|
};
|
|
17979
18718
|
var reviewSubmitTool = {
|
|
17980
18719
|
name: "review_submit",
|
|
17981
|
-
description: "Record
|
|
18720
|
+
description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.",
|
|
17982
18721
|
inputSchema: {
|
|
17983
18722
|
type: "object",
|
|
17984
18723
|
properties: {
|
|
@@ -18367,21 +19106,22 @@ Path: ${mcpJsonPath}`
|
|
|
18367
19106
|
|
|
18368
19107
|
// src/tools/orient.ts
|
|
18369
19108
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
18370
|
-
import { readFileSync, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
19109
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
18371
19110
|
import { join as join7 } from "path";
|
|
18372
19111
|
var orientTool = {
|
|
18373
19112
|
name: "orient",
|
|
18374
|
-
description: "Session orientation \u2014
|
|
19113
|
+
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.",
|
|
18375
19114
|
inputSchema: {
|
|
18376
19115
|
type: "object",
|
|
18377
19116
|
properties: {},
|
|
18378
19117
|
required: []
|
|
18379
19118
|
}
|
|
18380
19119
|
};
|
|
18381
|
-
function formatOrientSummary(health, buildInfo, hierarchy) {
|
|
19120
|
+
function formatOrientSummary(health, buildInfo, hierarchy, latestTag) {
|
|
18382
19121
|
const lines = [];
|
|
18383
19122
|
const cycleIsComplete = health.latestCycleStatus === "complete";
|
|
18384
|
-
|
|
19123
|
+
const tagSuffix = latestTag ? ` \u2014 ${latestTag}` : "";
|
|
19124
|
+
lines.push(`# Cycle ${health.cycleNumber}${cycleIsComplete ? " (complete)" : ""}${tagSuffix} \u2014 Orient`);
|
|
18385
19125
|
lines.push("");
|
|
18386
19126
|
if (health.connectionStatus !== "offline") {
|
|
18387
19127
|
const statusIcon = health.connectionStatus === "connected" ? "\u2713" : "\u26A0\uFE0F";
|
|
@@ -18517,10 +19257,22 @@ async function getHierarchyPosition(adapter2) {
|
|
|
18517
19257
|
return void 0;
|
|
18518
19258
|
}
|
|
18519
19259
|
}
|
|
19260
|
+
function getLatestGitTag(projectRoot) {
|
|
19261
|
+
try {
|
|
19262
|
+
return execFileSync3("git", ["describe", "--tags", "--abbrev=0"], {
|
|
19263
|
+
encoding: "utf-8",
|
|
19264
|
+
cwd: projectRoot,
|
|
19265
|
+
timeout: 2e3,
|
|
19266
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
19267
|
+
}).trim() || null;
|
|
19268
|
+
} catch {
|
|
19269
|
+
return null;
|
|
19270
|
+
}
|
|
19271
|
+
}
|
|
18520
19272
|
function checkNpmVersionDrift() {
|
|
18521
19273
|
try {
|
|
18522
19274
|
const pkgPath = join7(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
18523
|
-
const pkg = JSON.parse(
|
|
19275
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
18524
19276
|
const localVersion = pkg.version;
|
|
18525
19277
|
const packageName = pkg.name;
|
|
18526
19278
|
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
@@ -18556,6 +19308,10 @@ async function handleOrient(adapter2, config2) {
|
|
|
18556
19308
|
const cycleInCycle = cycleIsComplete ? 0 : allTasks.filter((t) => t.cycle === currentCycle && t.status === "In Cycle").length;
|
|
18557
19309
|
const cycleReady = cycleIsComplete ? 0 : allTasks.filter((t) => t.cycle === currentCycle && t.status === "Ready").length;
|
|
18558
19310
|
const cycleTotal = cycleInProgress + cycleInReview + cycleBacklog + cycleInCycle + cycleReady;
|
|
19311
|
+
const cycleDone = cycleIsComplete ? 0 : allTasks.filter((t) => t.cycle === currentCycle && t.status === "Done").length;
|
|
19312
|
+
if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
|
|
19313
|
+
buildResult.warnings.unshift(`\u26A0\uFE0F Cycle ${currentCycle} is complete \u2014 all ${cycleDone} task${cycleDone !== 1 ? "s" : ""} Done. Release has not been run. Run \`release\` now.`);
|
|
19314
|
+
}
|
|
18559
19315
|
const inProgressItems = buildResult.inProgress.map(
|
|
18560
19316
|
(t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
|
|
18561
19317
|
);
|
|
@@ -18588,9 +19344,25 @@ async function handleOrient(adapter2, config2) {
|
|
|
18588
19344
|
} catch {
|
|
18589
19345
|
}
|
|
18590
19346
|
}
|
|
19347
|
+
const latestTag = getLatestGitTag(config2.projectRoot);
|
|
18591
19348
|
const versionDrift = checkNpmVersionDrift();
|
|
18592
19349
|
const versionNote = versionDrift ? `
|
|
18593
19350
|
${versionDrift}` : "";
|
|
19351
|
+
let reconciliationNote = "";
|
|
19352
|
+
try {
|
|
19353
|
+
const mismatches = detectBoardMismatches(config2.projectRoot, allTasks);
|
|
19354
|
+
if (mismatches.codeAhead.length > 0 || mismatches.staleInProgress.length > 0) {
|
|
19355
|
+
const lines = ["\n\n## Reconciliation"];
|
|
19356
|
+
for (const m of mismatches.codeAhead) {
|
|
19357
|
+
lines.push(`\u26A0\uFE0F **${m.displayId}** \u2014 branch \`${m.branch}\` merged to main but task is still Backlog. Run \`board_edit\` to mark Done.`);
|
|
19358
|
+
}
|
|
19359
|
+
for (const m of mismatches.staleInProgress) {
|
|
19360
|
+
lines.push(`\u26A0\uFE0F **${m.displayId}** \u2014 In Progress but no feature branch found. May be stale or built without \`build_execute\`.`);
|
|
19361
|
+
}
|
|
19362
|
+
reconciliationNote = lines.join("\n");
|
|
19363
|
+
}
|
|
19364
|
+
} catch {
|
|
19365
|
+
}
|
|
18594
19366
|
let recsNote = "";
|
|
18595
19367
|
try {
|
|
18596
19368
|
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
@@ -18624,7 +19396,7 @@ ${versionDrift}` : "";
|
|
|
18624
19396
|
}
|
|
18625
19397
|
} catch {
|
|
18626
19398
|
}
|
|
18627
|
-
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy) + ttfvNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
|
|
19399
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag) + ttfvNote + reconciliationNote + recsNote + pendingReviewNote + patternsNote + versionNote + enrichmentNote);
|
|
18628
19400
|
} catch (err) {
|
|
18629
19401
|
const message = err instanceof Error ? err.message : String(err);
|
|
18630
19402
|
return errorResponse(`Orient failed: ${message}`);
|
|
@@ -18633,7 +19405,7 @@ ${versionDrift}` : "";
|
|
|
18633
19405
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
18634
19406
|
const claudeMdPath = join7(projectRoot, "CLAUDE.md");
|
|
18635
19407
|
if (!existsSync4(claudeMdPath)) return "";
|
|
18636
|
-
const content =
|
|
19408
|
+
const content = readFileSync2(claudeMdPath, "utf-8");
|
|
18637
19409
|
const additions = [];
|
|
18638
19410
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
18639
19411
|
additions.push(CLAUDE_MD_TIER_1);
|
|
@@ -19119,7 +19891,7 @@ ${result.userMessage}
|
|
|
19119
19891
|
}
|
|
19120
19892
|
|
|
19121
19893
|
// src/tools/doc-registry.ts
|
|
19122
|
-
import { readdirSync as readdirSync3, existsSync as existsSync5, readFileSync as
|
|
19894
|
+
import { readdirSync as readdirSync3, existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
19123
19895
|
import { join as join8, relative } from "path";
|
|
19124
19896
|
import { homedir as homedir2 } from "os";
|
|
19125
19897
|
var docRegisterTool = {
|
|
@@ -19279,7 +20051,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
19279
20051
|
}
|
|
19280
20052
|
function extractTitle(filePath) {
|
|
19281
20053
|
try {
|
|
19282
|
-
const content =
|
|
20054
|
+
const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
|
|
19283
20055
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
19284
20056
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
19285
20057
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -19293,7 +20065,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
19293
20065
|
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
19294
20066
|
}
|
|
19295
20067
|
const includePlans = args.include_plans ?? false;
|
|
19296
|
-
const registered = await adapter2.searchDocs({ limit: 500 });
|
|
20068
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
19297
20069
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
19298
20070
|
const docsDir = join8(config2.projectRoot, "docs");
|
|
19299
20071
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|