@papi-ai/server 0.7.14 → 0.7.15
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 +415 -95
- package/dist/prompts.js +19 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6733,6 +6733,25 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6733
6733
|
superseded_by = ${supersededBy ?? null},
|
|
6734
6734
|
updated_at = now()
|
|
6735
6735
|
WHERE id = ${id} AND project_id = ${this.projectId}
|
|
6736
|
+
`;
|
|
6737
|
+
}
|
|
6738
|
+
async updateDocAction(docId, actionIndex, update) {
|
|
6739
|
+
const doc = await this.getDoc(docId);
|
|
6740
|
+
if (!doc) throw new Error(`Doc not found: ${docId}`);
|
|
6741
|
+
const actions = [...doc.actions ?? []];
|
|
6742
|
+
if (actionIndex < 0 || actionIndex >= actions.length) {
|
|
6743
|
+
throw new Error(`Action index ${actionIndex} out of range (doc has ${actions.length} action(s))`);
|
|
6744
|
+
}
|
|
6745
|
+
actions[actionIndex] = {
|
|
6746
|
+
...actions[actionIndex],
|
|
6747
|
+
...update.status !== void 0 ? { status: update.status } : {},
|
|
6748
|
+
...update.linkedTaskId !== void 0 ? { linkedTaskId: update.linkedTaskId } : {}
|
|
6749
|
+
};
|
|
6750
|
+
await this.sql`
|
|
6751
|
+
UPDATE doc_registry
|
|
6752
|
+
SET actions = ${this.sql.json(actions)},
|
|
6753
|
+
updated_at = now()
|
|
6754
|
+
WHERE id = ${doc.id} AND project_id = ${this.projectId}
|
|
6736
6755
|
`;
|
|
6737
6756
|
}
|
|
6738
6757
|
async writeDogfoodEntries(entries) {
|
|
@@ -7607,7 +7626,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7607
7626
|
const rows = await this.sql`
|
|
7608
7627
|
SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
|
|
7609
7628
|
FROM stages
|
|
7610
|
-
WHERE project_id = ${this.projectId} AND status = '
|
|
7629
|
+
WHERE project_id = ${this.projectId} AND status = 'In Progress'
|
|
7611
7630
|
ORDER BY sort_order
|
|
7612
7631
|
LIMIT 1
|
|
7613
7632
|
`;
|
|
@@ -9486,9 +9505,9 @@ var init_git = __esm({
|
|
|
9486
9505
|
});
|
|
9487
9506
|
|
|
9488
9507
|
// src/index.ts
|
|
9489
|
-
import { readFileSync as
|
|
9508
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
9490
9509
|
import { createServer as createHttpServer } from "http";
|
|
9491
|
-
import { dirname as dirname2, join as
|
|
9510
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
9492
9511
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9493
9512
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9494
9513
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -9775,9 +9794,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
9775
9794
|
}
|
|
9776
9795
|
|
|
9777
9796
|
// src/server.ts
|
|
9778
|
-
import { readFileSync as
|
|
9797
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
9779
9798
|
import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
9780
|
-
import { join as
|
|
9799
|
+
import { join as join13, dirname } from "path";
|
|
9781
9800
|
import { fileURLToPath } from "url";
|
|
9782
9801
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9783
9802
|
import {
|
|
@@ -10763,6 +10782,7 @@ Standard planning cycle with full board review.
|
|
|
10763
10782
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10764
10783
|
**\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.
|
|
10765
10784
|
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.
|
|
10785
|
+
**Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
|
|
10766
10786
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
10767
10787
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
10768
10788
|
|
|
@@ -11001,6 +11021,7 @@ Standard planning cycle with full board review.
|
|
|
11001
11021
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
11002
11022
|
**\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.
|
|
11003
11023
|
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.
|
|
11024
|
+
**Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
|
|
11004
11025
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
11005
11026
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
11006
11027
|
|
|
@@ -11215,6 +11236,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
11215
11236
|
if (ctx.registeredDocs) {
|
|
11216
11237
|
parts.push("### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
11217
11238
|
}
|
|
11239
|
+
if (ctx.unactionedDocs) {
|
|
11240
|
+
parts.push(
|
|
11241
|
+
"### Unactioned Research (soft-warn)",
|
|
11242
|
+
"",
|
|
11243
|
+
"These registered docs still have pending actions. Before adding net-new tasks, consider whether one of these unactioned items should be promoted into this cycle via `doc_action_promote`. Surface this in your plan output so the user can decide.",
|
|
11244
|
+
"",
|
|
11245
|
+
ctx.unactionedDocs,
|
|
11246
|
+
""
|
|
11247
|
+
);
|
|
11248
|
+
}
|
|
11218
11249
|
if (ctx.carryForwardStaleness) {
|
|
11219
11250
|
parts.push("### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
11220
11251
|
}
|
|
@@ -11427,6 +11458,13 @@ ${AD_REJECTION_RULES}
|
|
|
11427
11458
|
|
|
11428
11459
|
**Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
|
|
11429
11460
|
|
|
11461
|
+
**Doc Action Staleness:** If a "### Doc Action Staleness" section is present, treat it as a research-to-action audit. For each entry:
|
|
11462
|
+
- **Stale** (no Done task, >20 cycles old): explicitly call it out in section 2 (Product Gaps) or 3 (Opportunities) \u2014 research that's been sitting unactioned this long is either (a) no longer relevant (recommend marking the doc archived) or (b) silently dropped (recommend promoting via \`doc_action_promote\` into the next plan).
|
|
11463
|
+
- **Completed but not closed** (linked task is Done): instruct in your review that the action should be marked resolved \u2014 it's done work the registry hasn't caught up to.
|
|
11464
|
+
- **Deferred** (<20 cycles, no matching Done task): only mention if it relates to a current strategic theme; otherwise leave it.
|
|
11465
|
+
|
|
11466
|
+
For every stale or unresolved item you call out, propose a concrete next step (promote, archive, or supersede) \u2014 don't just observe.
|
|
11467
|
+
|
|
11430
11468
|
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
11431
11469
|
|
|
11432
11470
|
6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
|
|
@@ -12108,6 +12146,7 @@ function applyContextTier(ctx, cycleCount) {
|
|
|
12108
12146
|
ctx.reviewPatterns = void 0;
|
|
12109
12147
|
ctx.horizonContext = void 0;
|
|
12110
12148
|
ctx.registeredDocs = void 0;
|
|
12149
|
+
ctx.unactionedDocs = void 0;
|
|
12111
12150
|
ctx.recentReviews = void 0;
|
|
12112
12151
|
ctx.strategyReviewCadence = void 0;
|
|
12113
12152
|
}
|
|
@@ -12277,6 +12316,32 @@ function formatStrategyRecommendations(recs, currentCycle) {
|
|
|
12277
12316
|
}
|
|
12278
12317
|
return sections.join("\n");
|
|
12279
12318
|
}
|
|
12319
|
+
function formatUnactionedDocs(docs) {
|
|
12320
|
+
const withPending = docs.map((d) => ({
|
|
12321
|
+
doc: d,
|
|
12322
|
+
pending: (d.actions ?? []).filter((a) => a.status === "pending")
|
|
12323
|
+
})).filter((entry) => entry.pending.length > 0);
|
|
12324
|
+
if (withPending.length === 0) return void 0;
|
|
12325
|
+
const MAX_DOCS = 3;
|
|
12326
|
+
const MAX_ACTIONS_PER_DOC = 2;
|
|
12327
|
+
const top = withPending.slice(0, MAX_DOCS);
|
|
12328
|
+
const lines = [];
|
|
12329
|
+
for (const { doc, pending } of top) {
|
|
12330
|
+
const cycleLabel = `C${doc.cycleUpdated ?? doc.cycleCreated ?? "?"}`;
|
|
12331
|
+
lines.push(`- **${doc.title}** (${doc.path}) [${cycleLabel}, ${pending.length} pending]`);
|
|
12332
|
+
for (const action of pending.slice(0, MAX_ACTIONS_PER_DOC)) {
|
|
12333
|
+
const desc = action.description.length > 100 ? `${action.description.slice(0, 97)}\u2026` : action.description;
|
|
12334
|
+
lines.push(` \u2192 ${desc}`);
|
|
12335
|
+
}
|
|
12336
|
+
if (pending.length > MAX_ACTIONS_PER_DOC) {
|
|
12337
|
+
lines.push(` \u2192 \u2026and ${pending.length - MAX_ACTIONS_PER_DOC} more`);
|
|
12338
|
+
}
|
|
12339
|
+
}
|
|
12340
|
+
if (withPending.length > MAX_DOCS) {
|
|
12341
|
+
lines.push(`_\u2026and ${withPending.length - MAX_DOCS} more doc(s) with pending actions._`);
|
|
12342
|
+
}
|
|
12343
|
+
return lines.join("\n");
|
|
12344
|
+
}
|
|
12280
12345
|
function formatEstimationCalibration(rows) {
|
|
12281
12346
|
const total = rows.reduce((sum, r) => sum + r.count, 0);
|
|
12282
12347
|
const accurate = rows.filter((r) => r.accuracyLabel === "accurate").reduce((sum, r) => sum + r.count, 0);
|
|
@@ -12616,11 +12681,13 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
12616
12681
|
}
|
|
12617
12682
|
}
|
|
12618
12683
|
let registeredDocsText;
|
|
12684
|
+
let unactionedDocsTextLean;
|
|
12619
12685
|
if (docsResult.status === "fulfilled" && docsResult.value && docsResult.value.length > 0) {
|
|
12620
12686
|
const docs = docsResult.value;
|
|
12621
12687
|
const lines = docs.map((d) => `- **${d.title}** (${d.type}) \u2014 ${d.summary}`);
|
|
12622
12688
|
registeredDocsText = `${docs.length} active research doc(s):
|
|
12623
12689
|
${lines.join("\n")}`;
|
|
12690
|
+
unactionedDocsTextLean = formatUnactionedDocs(docs);
|
|
12624
12691
|
}
|
|
12625
12692
|
const boardFlags = detectBoardFlagsFromText(leanSummary.board);
|
|
12626
12693
|
let carryForwardStalenessLean;
|
|
@@ -12672,6 +12739,7 @@ ${lines.join("\n")}`;
|
|
|
12672
12739
|
discoveryCanvas: discoveryCanvasText,
|
|
12673
12740
|
estimationCalibration: estimationCalibrationText,
|
|
12674
12741
|
registeredDocs: registeredDocsText,
|
|
12742
|
+
unactionedDocs: unactionedDocsTextLean,
|
|
12675
12743
|
focus,
|
|
12676
12744
|
boardFlags,
|
|
12677
12745
|
carryForwardStaleness: carryForwardStalenessLean,
|
|
@@ -12749,9 +12817,24 @@ ${lines.join("\n")}`;
|
|
|
12749
12817
|
const metricsSnapshots = filteredRaw.length > 0 ? filteredRaw : computeSnapshotsFromBuildReports(allReportsForPatterns);
|
|
12750
12818
|
const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
|
|
12751
12819
|
const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
|
|
12820
|
+
const strippedTasks = stripTasksForPlan(tasks);
|
|
12821
|
+
const boardFlagsFull = detectBoardFlags(tasks);
|
|
12822
|
+
const horizonCtx = buildHorizonContext(phases, tasks) ?? void 0;
|
|
12823
|
+
const ACTIVE_STATUSES2 = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
|
|
12824
|
+
const p3Excluded = strippedTasks.filter(
|
|
12825
|
+
(t2) => t2.priority === "P3 Low" && !ACTIVE_STATUSES2.has(t2.status)
|
|
12826
|
+
);
|
|
12827
|
+
const plannerTasks = strippedTasks.filter(
|
|
12828
|
+
(t2) => t2.priority !== "P3 Low" || ACTIVE_STATUSES2.has(t2.status)
|
|
12829
|
+
);
|
|
12830
|
+
if (p3Excluded.length > 0) {
|
|
12831
|
+
console.error(`[plan-perf] board tiering: excluded ${p3Excluded.length} P3 Low tasks from planner context`);
|
|
12832
|
+
}
|
|
12752
12833
|
let registeredDocsTextFull;
|
|
12834
|
+
let unactionedDocsTextFull;
|
|
12753
12835
|
if (docsResultFull.status === "fulfilled" && docsResultFull.value && docsResultFull.value.length > 0) {
|
|
12754
12836
|
const allDocs = docsResultFull.value;
|
|
12837
|
+
unactionedDocsTextFull = formatUnactionedDocs(allDocs);
|
|
12755
12838
|
const taskModules = new Set(plannerTasks.map((t2) => t2.module?.toLowerCase()).filter(Boolean));
|
|
12756
12839
|
const taskEpics = new Set(plannerTasks.map((t2) => t2.epic?.toLowerCase()).filter(Boolean));
|
|
12757
12840
|
const HIGH_VALUE_TYPES = /* @__PURE__ */ new Set(["architecture", "guide", "research"]);
|
|
@@ -12797,19 +12880,6 @@ ${logLines}`);
|
|
|
12797
12880
|
const filtered = reports.filter((r) => (r.cycle ?? 0) >= cutoff);
|
|
12798
12881
|
return filtered.length > 0 ? filtered : reports.slice(0, 5);
|
|
12799
12882
|
})() : reports;
|
|
12800
|
-
const strippedTasks = stripTasksForPlan(tasks);
|
|
12801
|
-
const boardFlagsFull = detectBoardFlags(tasks);
|
|
12802
|
-
const horizonCtx = buildHorizonContext(phases, tasks) ?? void 0;
|
|
12803
|
-
const ACTIVE_STATUSES2 = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
|
|
12804
|
-
const p3Excluded = strippedTasks.filter(
|
|
12805
|
-
(t2) => t2.priority === "P3 Low" && !ACTIVE_STATUSES2.has(t2.status)
|
|
12806
|
-
);
|
|
12807
|
-
const plannerTasks = strippedTasks.filter(
|
|
12808
|
-
(t2) => t2.priority !== "P3 Low" || ACTIVE_STATUSES2.has(t2.status)
|
|
12809
|
-
);
|
|
12810
|
-
if (p3Excluded.length > 0) {
|
|
12811
|
-
console.error(`[plan-perf] board tiering: excluded ${p3Excluded.length} P3 Low tasks from planner context`);
|
|
12812
|
-
}
|
|
12813
12883
|
const targetCycle = health.totalCycles + 1;
|
|
12814
12884
|
const preAssigned = plannerTasks.filter((t2) => t2.cycle === targetCycle);
|
|
12815
12885
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
@@ -12835,6 +12905,7 @@ ${logLines}`);
|
|
|
12835
12905
|
taskComments: taskCommentsTextFull,
|
|
12836
12906
|
discoveryCanvas: discoveryCanvasTextFull,
|
|
12837
12907
|
registeredDocs: registeredDocsTextFull,
|
|
12908
|
+
unactionedDocs: unactionedDocsTextFull,
|
|
12838
12909
|
focus,
|
|
12839
12910
|
boardFlags: boardFlagsFull,
|
|
12840
12911
|
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
@@ -13794,17 +13865,19 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13794
13865
|
const expectedCycleNumber = lastPrepareCycleNumber;
|
|
13795
13866
|
const skipHandoffsCached = lastPrepareSkipHandoffs;
|
|
13796
13867
|
const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
|
|
13797
|
-
const
|
|
13798
|
-
|
|
13868
|
+
const expectedNewCycleNumber = expectedCycleNumber !== void 0 ? expectedCycleNumber + 1 : void 0;
|
|
13869
|
+
const newCycleNumber = !isNaN(rawCycleNumber) ? rawCycleNumber : expectedNewCycleNumber !== void 0 ? expectedNewCycleNumber : NaN;
|
|
13870
|
+
if (isNaN(newCycleNumber)) {
|
|
13799
13871
|
return errorResponse(
|
|
13800
13872
|
"cycle_number is required for apply mode. Pass the cycle_number from the prepare phase output."
|
|
13801
13873
|
);
|
|
13802
13874
|
}
|
|
13803
|
-
if (
|
|
13875
|
+
if (expectedNewCycleNumber !== void 0 && newCycleNumber !== expectedNewCycleNumber) {
|
|
13804
13876
|
return errorResponse(
|
|
13805
|
-
`cycle_number mismatch: prepare phase
|
|
13877
|
+
`cycle_number mismatch: prepare phase is for cycle ${expectedNewCycleNumber} but apply received ${newCycleNumber}. Pass cycle_number: ${expectedNewCycleNumber} to match the prepare output.`
|
|
13806
13878
|
);
|
|
13807
13879
|
}
|
|
13880
|
+
const cycleNumber = newCycleNumber - 1;
|
|
13808
13881
|
lastPrepareContextHashes = void 0;
|
|
13809
13882
|
lastPrepareUserMessage = void 0;
|
|
13810
13883
|
lastPrepareContextBytes = void 0;
|
|
@@ -13858,7 +13931,7 @@ When done, call \`plan\` again with:
|
|
|
13858
13931
|
- \`mode\`: "apply"
|
|
13859
13932
|
- \`llm_response\`: your complete output (both parts)
|
|
13860
13933
|
- \`plan_mode\`: "${result.mode}"
|
|
13861
|
-
- \`cycle_number\`: ${result.cycleNumber}
|
|
13934
|
+
- \`cycle_number\`: ${result.cycleNumber + 1}
|
|
13862
13935
|
- \`strategy_review_warning\`: "${result.strategyReviewWarning.replace(/"/g, '\\"')}"
|
|
13863
13936
|
|
|
13864
13937
|
---
|
|
@@ -18703,8 +18776,20 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
18703
18776
|
const correctionsCount = args.corrections_count;
|
|
18704
18777
|
const deadEnds = args.dead_ends;
|
|
18705
18778
|
const rawBriefImplications = args.brief_implications;
|
|
18706
|
-
|
|
18707
|
-
|
|
18779
|
+
const reportFieldChecks = [
|
|
18780
|
+
["completed", completed],
|
|
18781
|
+
["effort", effort],
|
|
18782
|
+
["estimated_effort", estimatedEffort],
|
|
18783
|
+
["surprises", surprises],
|
|
18784
|
+
["discovered_issues", discoveredIssues],
|
|
18785
|
+
["architecture_notes", architectureNotes]
|
|
18786
|
+
];
|
|
18787
|
+
const missing = reportFieldChecks.filter(([, v]) => !v).map(([k]) => k);
|
|
18788
|
+
if (missing.length > 0) {
|
|
18789
|
+
const sizes = reportFieldChecks.map(([k, v]) => `${k}=${v === void 0 ? "undefined" : v.length}`).join(", ");
|
|
18790
|
+
return errorResponse(
|
|
18791
|
+
`build_execute with report data is missing required field(s): ${missing.join(", ")}. If you sent values for these, the transport may have dropped them \u2014 re-send with shorter content for the affected field(s) or split the report. Field sizes (chars): ${sizes}.`
|
|
18792
|
+
);
|
|
18708
18793
|
}
|
|
18709
18794
|
const parsedEffort = parseEffortSize(effort);
|
|
18710
18795
|
const parsedEstimatedEffort = parseEffortSize(estimatedEffort);
|
|
@@ -21516,10 +21601,117 @@ ${lines.join("\n")}`;
|
|
|
21516
21601
|
// src/tools/orient.ts
|
|
21517
21602
|
init_git();
|
|
21518
21603
|
|
|
21604
|
+
// src/lib/skill-detection.ts
|
|
21605
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
|
|
21606
|
+
import { join as join8 } from "path";
|
|
21607
|
+
function readPackageJson(projectRoot) {
|
|
21608
|
+
const path5 = join8(projectRoot, "package.json");
|
|
21609
|
+
if (!existsSync5(path5)) return null;
|
|
21610
|
+
try {
|
|
21611
|
+
const raw = readFileSync2(path5, "utf-8");
|
|
21612
|
+
return JSON.parse(raw);
|
|
21613
|
+
} catch {
|
|
21614
|
+
return null;
|
|
21615
|
+
}
|
|
21616
|
+
}
|
|
21617
|
+
function allDeps(pkg) {
|
|
21618
|
+
if (!pkg) return {};
|
|
21619
|
+
return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
21620
|
+
}
|
|
21621
|
+
function hasDependencyMatching(deps, pattern) {
|
|
21622
|
+
for (const name of Object.keys(deps)) {
|
|
21623
|
+
if (pattern.test(name)) return true;
|
|
21624
|
+
}
|
|
21625
|
+
return false;
|
|
21626
|
+
}
|
|
21627
|
+
function hasGitHubWorkflows(projectRoot) {
|
|
21628
|
+
const dir = join8(projectRoot, ".github", "workflows");
|
|
21629
|
+
if (!existsSync5(dir)) return false;
|
|
21630
|
+
try {
|
|
21631
|
+
const entries = readdirSync4(dir);
|
|
21632
|
+
return entries.some((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
|
|
21633
|
+
} catch {
|
|
21634
|
+
return false;
|
|
21635
|
+
}
|
|
21636
|
+
}
|
|
21637
|
+
function envExampleMentionsStaging(projectRoot) {
|
|
21638
|
+
const path5 = join8(projectRoot, ".env.example");
|
|
21639
|
+
if (!existsSync5(path5)) return false;
|
|
21640
|
+
try {
|
|
21641
|
+
const raw = readFileSync2(path5, "utf-8");
|
|
21642
|
+
return /\b(STAGING_URL|STAGING_API|STAGING_HOST|NEXT_PUBLIC_STAGING)/i.test(raw);
|
|
21643
|
+
} catch {
|
|
21644
|
+
return false;
|
|
21645
|
+
}
|
|
21646
|
+
}
|
|
21647
|
+
function hasVercelConfig(projectRoot) {
|
|
21648
|
+
if (existsSync5(join8(projectRoot, "vercel.json"))) return true;
|
|
21649
|
+
const vercelDir = join8(projectRoot, ".vercel");
|
|
21650
|
+
if (!existsSync5(vercelDir)) return false;
|
|
21651
|
+
try {
|
|
21652
|
+
return statSync3(vercelDir).isDirectory();
|
|
21653
|
+
} catch {
|
|
21654
|
+
return false;
|
|
21655
|
+
}
|
|
21656
|
+
}
|
|
21657
|
+
function scanForSkillSignals(projectRoot) {
|
|
21658
|
+
const proposals = [];
|
|
21659
|
+
const pkg = readPackageJson(projectRoot);
|
|
21660
|
+
const deps = allDeps(pkg);
|
|
21661
|
+
if (hasGitHubWorkflows(projectRoot)) {
|
|
21662
|
+
proposals.push({
|
|
21663
|
+
id: "gh-actions-debug",
|
|
21664
|
+
name: "GitHub Actions debugger",
|
|
21665
|
+
rationale: "Detected .github/workflows/ \u2014 a skill for inspecting CI failures and re-running jobs from Claude Code can shorten the debug loop.",
|
|
21666
|
+
hint: 'Search Claude Code skill registry for "gh-actions" or install via `claude skills add gh-actions-debug`.'
|
|
21667
|
+
});
|
|
21668
|
+
}
|
|
21669
|
+
if (hasDependencyMatching(deps, /^@sentry\//) || hasDependencyMatching(deps, /^@datadog\//)) {
|
|
21670
|
+
proposals.push({
|
|
21671
|
+
id: "error-tracking",
|
|
21672
|
+
name: "Error tracking helper",
|
|
21673
|
+
rationale: "Detected @sentry/* or @datadog/* in package.json \u2014 a skill that fetches recent error events into your context can speed up triage.",
|
|
21674
|
+
hint: 'Search Claude Code skill registry for "sentry" or "datadog".'
|
|
21675
|
+
});
|
|
21676
|
+
}
|
|
21677
|
+
if (hasVercelConfig(projectRoot)) {
|
|
21678
|
+
proposals.push({
|
|
21679
|
+
id: "read-vercel-logs",
|
|
21680
|
+
name: "Vercel deploy logs",
|
|
21681
|
+
rationale: "Detected vercel.json or .vercel/ \u2014 a skill for pulling deploy + runtime logs from Vercel directly into Claude Code helps when builds fail or runtime errors land.",
|
|
21682
|
+
hint: 'Search Claude Code skill registry for "vercel" or install `vercel:logs`.'
|
|
21683
|
+
});
|
|
21684
|
+
}
|
|
21685
|
+
if (envExampleMentionsStaging(projectRoot)) {
|
|
21686
|
+
proposals.push({
|
|
21687
|
+
id: "staging-environment",
|
|
21688
|
+
name: "Staging-environment helper",
|
|
21689
|
+
rationale: "Detected STAGING_* variables in .env.example \u2014 a skill for switching env contexts and seeding staging data can speed up pre-prod testing.",
|
|
21690
|
+
hint: 'Search Claude Code skill registry for "staging" or define your own.'
|
|
21691
|
+
});
|
|
21692
|
+
}
|
|
21693
|
+
return proposals;
|
|
21694
|
+
}
|
|
21695
|
+
function formatSkillProposals(proposals) {
|
|
21696
|
+
if (proposals.length === 0) return "";
|
|
21697
|
+
const lines = ["", "## Skill proposals", "_PAPI scanned your project once and noticed tooling that often pairs with a Claude Code skill. Each proposal is one-shot \u2014 they won't resurface._"];
|
|
21698
|
+
for (const p of proposals) {
|
|
21699
|
+
lines.push("");
|
|
21700
|
+
lines.push(`### ${p.name}`);
|
|
21701
|
+
lines.push(p.rationale);
|
|
21702
|
+
if (p.hint) {
|
|
21703
|
+
lines.push(`_${p.hint}_`);
|
|
21704
|
+
}
|
|
21705
|
+
lines.push("**[Yes]** install it now \xB7 **[Not now]** dismiss for this project");
|
|
21706
|
+
}
|
|
21707
|
+
return "\n" + lines.join("\n");
|
|
21708
|
+
}
|
|
21709
|
+
|
|
21519
21710
|
// src/tools/doc-registry.ts
|
|
21520
|
-
import { readdirSync as
|
|
21521
|
-
import { join as
|
|
21711
|
+
import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
21712
|
+
import { join as join9, relative } from "path";
|
|
21522
21713
|
import { homedir as homedir2 } from "os";
|
|
21714
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
21523
21715
|
var docRegisterTool = {
|
|
21524
21716
|
name: "doc_register",
|
|
21525
21717
|
description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
|
|
@@ -21662,12 +21854,12 @@ ${d.summary}
|
|
|
21662
21854
|
${lines.join("\n---\n\n")}`);
|
|
21663
21855
|
}
|
|
21664
21856
|
function scanMdFiles(dir, rootDir) {
|
|
21665
|
-
if (!
|
|
21857
|
+
if (!existsSync6(dir)) return [];
|
|
21666
21858
|
const files = [];
|
|
21667
21859
|
try {
|
|
21668
|
-
const entries =
|
|
21860
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
21669
21861
|
for (const entry of entries) {
|
|
21670
|
-
const full =
|
|
21862
|
+
const full = join9(dir, entry.name);
|
|
21671
21863
|
if (entry.isDirectory()) {
|
|
21672
21864
|
files.push(...scanMdFiles(full, rootDir));
|
|
21673
21865
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -21680,7 +21872,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
21680
21872
|
}
|
|
21681
21873
|
function extractTitle(filePath) {
|
|
21682
21874
|
try {
|
|
21683
|
-
const content =
|
|
21875
|
+
const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
|
|
21684
21876
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
21685
21877
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
21686
21878
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -21696,17 +21888,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21696
21888
|
const includePlans = args.include_plans ?? false;
|
|
21697
21889
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21698
21890
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21699
|
-
const docsDir =
|
|
21891
|
+
const docsDir = join9(config2.projectRoot, "docs");
|
|
21700
21892
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21701
21893
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
21702
21894
|
let unregisteredPlans = [];
|
|
21703
21895
|
if (includePlans) {
|
|
21704
|
-
const plansDir =
|
|
21705
|
-
if (
|
|
21896
|
+
const plansDir = join9(homedir2(), ".claude", "plans");
|
|
21897
|
+
if (existsSync6(plansDir)) {
|
|
21706
21898
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
21707
21899
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
21708
21900
|
path: f,
|
|
21709
|
-
title: extractTitle(
|
|
21901
|
+
title: extractTitle(join9(plansDir, f.replace("plans/", "")))
|
|
21710
21902
|
}));
|
|
21711
21903
|
}
|
|
21712
21904
|
}
|
|
@@ -21717,7 +21909,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21717
21909
|
if (unregisteredDocs.length > 0) {
|
|
21718
21910
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
21719
21911
|
for (const f of unregisteredDocs) {
|
|
21720
|
-
const title = extractTitle(
|
|
21912
|
+
const title = extractTitle(join9(config2.projectRoot, f));
|
|
21721
21913
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
21722
21914
|
}
|
|
21723
21915
|
}
|
|
@@ -21730,10 +21922,106 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21730
21922
|
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
21731
21923
|
return textResponse(lines.join("\n"));
|
|
21732
21924
|
}
|
|
21925
|
+
var docActionPromoteTool = {
|
|
21926
|
+
name: "doc_action_promote",
|
|
21927
|
+
description: "Promote a single pending action from a registered doc into a Backlog task. The new task gets a `Reference:` line pointing to the source doc, and the doc action is marked resolved with `linkedTaskId` set. Use to close the research-to-action loop \u2014 turn unactioned findings into trackable cycle work. Identify the doc by `doc_path` (preferred) or `doc_id`, and the action by 0-based `action_index` (as listed in `doc_search` output).",
|
|
21928
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21929
|
+
inputSchema: {
|
|
21930
|
+
type: "object",
|
|
21931
|
+
properties: {
|
|
21932
|
+
doc_path: { type: "string", description: 'Path of the source doc (e.g. "docs/research/funding-landscape.md"). Either this or doc_id is required.' },
|
|
21933
|
+
doc_id: { type: "string", description: "UUID of the source doc. Either this or doc_path is required." },
|
|
21934
|
+
action_index: { type: "number", description: "0-based index of the action in the doc's actions array." },
|
|
21935
|
+
title: { type: "string", description: "Optional task title override. Defaults to the action description (truncated)." },
|
|
21936
|
+
priority: { type: "string", enum: ["P0 Critical", "P1 High", "P2 Medium", "P3 Low"], description: 'Task priority. Defaults to "P2 Medium".' },
|
|
21937
|
+
complexity: { type: "string", enum: ["XS", "Small", "Medium", "Large", "XL"], description: 'Task complexity. Defaults to "Small".' },
|
|
21938
|
+
module: { type: "string", description: 'Task module. Defaults to "Core".' },
|
|
21939
|
+
epic: { type: "string", description: 'Task epic. Defaults to "Platform".' },
|
|
21940
|
+
notes: { type: "string", description: "Additional notes. A `Reference:` line pointing at the source doc is always prepended." }
|
|
21941
|
+
},
|
|
21942
|
+
required: ["action_index"]
|
|
21943
|
+
}
|
|
21944
|
+
};
|
|
21945
|
+
async function handleDocActionPromote(adapter2, args) {
|
|
21946
|
+
if (!adapter2.searchDocs || !adapter2.getDoc || !adapter2.updateDocAction) {
|
|
21947
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
21948
|
+
}
|
|
21949
|
+
const docPath = args.doc_path;
|
|
21950
|
+
const docId = args.doc_id;
|
|
21951
|
+
const actionIndex = args.action_index;
|
|
21952
|
+
if (!docPath && !docId) {
|
|
21953
|
+
return errorResponse("Either doc_path or doc_id is required.");
|
|
21954
|
+
}
|
|
21955
|
+
if (typeof actionIndex !== "number" || actionIndex < 0) {
|
|
21956
|
+
return errorResponse("action_index is required and must be a non-negative number.");
|
|
21957
|
+
}
|
|
21958
|
+
const doc = await adapter2.getDoc(docId ?? docPath);
|
|
21959
|
+
if (!doc) {
|
|
21960
|
+
return errorResponse(`Doc not found: ${docId ?? docPath}`);
|
|
21961
|
+
}
|
|
21962
|
+
const actions = doc.actions ?? [];
|
|
21963
|
+
if (actionIndex >= actions.length) {
|
|
21964
|
+
return errorResponse(`action_index ${actionIndex} out of range \u2014 doc "${doc.title}" has ${actions.length} action(s).`);
|
|
21965
|
+
}
|
|
21966
|
+
const action = actions[actionIndex];
|
|
21967
|
+
if (action.status === "resolved") {
|
|
21968
|
+
return errorResponse(`Action ${actionIndex} on "${doc.title}" is already resolved${action.linkedTaskId ? ` (linked to ${action.linkedTaskId})` : ""}.`);
|
|
21969
|
+
}
|
|
21970
|
+
const VALID_PRIORITIES3 = /* @__PURE__ */ new Set(["P0 Critical", "P1 High", "P2 Medium", "P3 Low"]);
|
|
21971
|
+
const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
21972
|
+
const priority = args.priority && VALID_PRIORITIES3.has(args.priority) ? args.priority : "P2 Medium";
|
|
21973
|
+
const complexity = args.complexity && VALID_COMPLEXITIES2.has(args.complexity) ? args.complexity : "Small";
|
|
21974
|
+
const taskModule = args.module || "Core";
|
|
21975
|
+
const taskEpic = args.epic || "Platform";
|
|
21976
|
+
const titleOverride = args.title?.trim();
|
|
21977
|
+
const fallbackTitle = action.description.length > 100 ? `${action.description.slice(0, 97)}\u2026` : action.description;
|
|
21978
|
+
const taskTitle = titleOverride || fallbackTitle;
|
|
21979
|
+
const referenceLine = `Reference: ${doc.path}`;
|
|
21980
|
+
const userNotes = args.notes?.trim();
|
|
21981
|
+
const notes = userNotes ? `${referenceLine}
|
|
21982
|
+
|
|
21983
|
+
${userNotes}` : referenceLine;
|
|
21984
|
+
const phases = await adapter2.readPhases();
|
|
21985
|
+
const phase = resolveCurrentPhase(phases);
|
|
21986
|
+
const health = await adapter2.getCycleHealth();
|
|
21987
|
+
const task = await adapter2.createTask({
|
|
21988
|
+
uuid: randomUUID15(),
|
|
21989
|
+
displayId: "",
|
|
21990
|
+
title: taskTitle,
|
|
21991
|
+
status: "Backlog",
|
|
21992
|
+
priority,
|
|
21993
|
+
complexity,
|
|
21994
|
+
module: taskModule,
|
|
21995
|
+
epic: taskEpic,
|
|
21996
|
+
phase,
|
|
21997
|
+
owner: "TBD",
|
|
21998
|
+
reviewed: false,
|
|
21999
|
+
createdCycle: health.totalCycles,
|
|
22000
|
+
notes,
|
|
22001
|
+
taskType: "task",
|
|
22002
|
+
maturity: "raw",
|
|
22003
|
+
docRef: doc.path,
|
|
22004
|
+
source: "llm"
|
|
22005
|
+
});
|
|
22006
|
+
await adapter2.updateDocAction(doc.id, actionIndex, {
|
|
22007
|
+
status: "resolved",
|
|
22008
|
+
linkedTaskId: task.displayId || task.id
|
|
22009
|
+
});
|
|
22010
|
+
const remaining = actions.filter((a, i) => i !== actionIndex && a.status === "pending").length;
|
|
22011
|
+
const remainingNote = remaining > 0 ? `${remaining} pending action(s) still on this doc.` : "All actions on this doc are now resolved.";
|
|
22012
|
+
return textResponse(
|
|
22013
|
+
`**Promoted action \u2192 ${task.displayId || task.id}**
|
|
22014
|
+
- **Source doc:** ${doc.title} (${doc.path})
|
|
22015
|
+
- **Action:** ${action.description}
|
|
22016
|
+
- **Task:** ${taskTitle} [${priority} | ${complexity}]
|
|
22017
|
+
- **Module/Epic:** ${taskModule} / ${taskEpic}
|
|
22018
|
+
- ${remainingNote}`
|
|
22019
|
+
);
|
|
22020
|
+
}
|
|
21733
22021
|
|
|
21734
22022
|
// src/services/session-guidance.ts
|
|
21735
|
-
import { existsSync as
|
|
21736
|
-
import { join as
|
|
22023
|
+
import { existsSync as existsSync7 } from "fs";
|
|
22024
|
+
import { join as join10 } from "path";
|
|
21737
22025
|
var state = {
|
|
21738
22026
|
toolCallCount: 0,
|
|
21739
22027
|
lastOrientAt: null,
|
|
@@ -21754,8 +22042,8 @@ async function buildSessionGuidance(adapter2, projectRoot) {
|
|
|
21754
22042
|
const signals = [];
|
|
21755
22043
|
try {
|
|
21756
22044
|
if (adapter2.searchDocs) {
|
|
21757
|
-
const researchDir =
|
|
21758
|
-
if (
|
|
22045
|
+
const researchDir = join10(projectRoot, "docs", "research");
|
|
22046
|
+
if (existsSync7(researchDir)) {
|
|
21759
22047
|
const files = scanMdFiles(researchDir, projectRoot);
|
|
21760
22048
|
if (files.length > 0) {
|
|
21761
22049
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -21792,8 +22080,8 @@ async function buildSessionGuidance(adapter2, projectRoot) {
|
|
|
21792
22080
|
|
|
21793
22081
|
// src/tools/orient.ts
|
|
21794
22082
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
21795
|
-
import { readFileSync as
|
|
21796
|
-
import { join as
|
|
22083
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync8 } from "fs";
|
|
22084
|
+
import { join as join11 } from "path";
|
|
21797
22085
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["cowork", "api"]);
|
|
21798
22086
|
var VALID_ENVS = /* @__PURE__ */ new Set(["claude-code", "cowork", "api", "unknown"]);
|
|
21799
22087
|
function normaliseEnvironment(raw) {
|
|
@@ -21949,10 +22237,10 @@ async function getHierarchyPosition(adapter2) {
|
|
|
21949
22237
|
adapter2.queryBoard()
|
|
21950
22238
|
]);
|
|
21951
22239
|
if (horizons.length === 0) return void 0;
|
|
21952
|
-
const
|
|
21953
|
-
const activeHorizon = horizons.find(
|
|
22240
|
+
const isInProgress = (s) => s.status === "In Progress";
|
|
22241
|
+
const activeHorizon = horizons.find(isInProgress) || horizons[0];
|
|
21954
22242
|
const horizonStages = stages.filter((s) => s.horizonId === activeHorizon.id).sort((a, b2) => (a.sortOrder ?? 0) - (b2.sortOrder ?? 0));
|
|
21955
|
-
const activeStage = horizonStages.find(
|
|
22243
|
+
const activeStage = horizonStages.find(isInProgress) || horizonStages.find((s) => s.status !== "Done") || horizonStages[0];
|
|
21956
22244
|
if (!activeStage) return void 0;
|
|
21957
22245
|
const stagePhases = phases.filter((p) => p.stageId === activeStage.id);
|
|
21958
22246
|
const activePhases = stagePhases.filter((p) => p.status === "In Progress");
|
|
@@ -21993,8 +22281,8 @@ function getLatestGitTag(projectRoot) {
|
|
|
21993
22281
|
}
|
|
21994
22282
|
function checkNpmVersionDrift() {
|
|
21995
22283
|
try {
|
|
21996
|
-
const pkgPath =
|
|
21997
|
-
const pkg = JSON.parse(
|
|
22284
|
+
const pkgPath = join11(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
22285
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
21998
22286
|
const localVersion = pkg.version;
|
|
21999
22287
|
const packageName = pkg.name;
|
|
22000
22288
|
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
@@ -22010,6 +22298,44 @@ function checkNpmVersionDrift() {
|
|
|
22010
22298
|
return null;
|
|
22011
22299
|
}
|
|
22012
22300
|
}
|
|
22301
|
+
var ALERT_CAP = 10;
|
|
22302
|
+
var UNACTIONED_CAP = 5;
|
|
22303
|
+
function formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds) {
|
|
22304
|
+
const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
|
|
22305
|
+
const unactionedAll = candidateLearnings.filter((l) => !closedTaskIds.has(l.taskId)).map((l) => ({ ...l, severity: l.severity ?? "P3" }));
|
|
22306
|
+
const allAlerts = unactionedAll.filter((l) => l.severity === "P0" || l.severity === "P1").sort(byRecency);
|
|
22307
|
+
const allLowSev = unactionedAll.filter((l) => l.severity === "P2" || l.severity === "P3").sort(byRecency);
|
|
22308
|
+
const totalP2 = allLowSev.filter((l) => l.severity === "P2").length;
|
|
22309
|
+
const totalP3 = allLowSev.filter((l) => l.severity === "P3").length;
|
|
22310
|
+
const alerts = allAlerts.slice(0, ALERT_CAP);
|
|
22311
|
+
const unactioned = allLowSev.slice(0, UNACTIONED_CAP);
|
|
22312
|
+
let alertsNote = "";
|
|
22313
|
+
let unactionedIssuesNote = "";
|
|
22314
|
+
if (allAlerts.length > 0) {
|
|
22315
|
+
const header = allAlerts.length > ALERT_CAP ? `${allAlerts.length} P0/P1 issues discovered during recent builds (showing ${ALERT_CAP} most recent). These are NOT pullable tasks \u2014 they're observations awaiting triage.` : `${allAlerts.length} P0/P1 issue${allAlerts.length !== 1 ? "s" : ""} discovered during recent builds. These are NOT pullable tasks \u2014 they're observations awaiting triage.`;
|
|
22316
|
+
const lines = ["\n\n## \u{1F6A8} Alerts \u2014 Discovered Issues from Recent Builds (not tasks)", header];
|
|
22317
|
+
for (const issue of alerts) {
|
|
22318
|
+
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
22319
|
+
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
22320
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
22321
|
+
}
|
|
22322
|
+
lines.push("_Escalate: run `idea` with P1 priority to log as a backlog task, or `board_edit` if already handled._");
|
|
22323
|
+
alertsNote = lines.join("\n");
|
|
22324
|
+
}
|
|
22325
|
+
if (allLowSev.length > 0) {
|
|
22326
|
+
const totalLow = totalP2 + totalP3;
|
|
22327
|
+
const header = totalLow > UNACTIONED_CAP ? `${totalP2} P2 \xB7 ${totalP3} P3 issues discovered during recent builds (showing ${UNACTIONED_CAP} most recent). These are NOT pullable tasks \u2014 they're observations awaiting triage.` : `${totalP2} P2 \xB7 ${totalP3} P3 issues discovered during recent builds. These are NOT pullable tasks \u2014 they're observations awaiting triage.`;
|
|
22328
|
+
const lines = ["\n\n## Discovered Issues from Recent Builds (not tasks)", header];
|
|
22329
|
+
for (const issue of unactioned) {
|
|
22330
|
+
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
22331
|
+
lines.push(`- **${issue.severity}**: ${desc}`);
|
|
22332
|
+
lines.push(` \u21B3 discovered during ${issue.taskId} (C${issue.cycleNumber})`);
|
|
22333
|
+
}
|
|
22334
|
+
lines.push("_Run `idea` to log these as backlog tasks, or `board_edit` if already handled._");
|
|
22335
|
+
unactionedIssuesNote = lines.join("\n");
|
|
22336
|
+
}
|
|
22337
|
+
return { alertsNote, unactionedIssuesNote };
|
|
22338
|
+
}
|
|
22013
22339
|
async function handleOrient(adapter2, config2, args = {}) {
|
|
22014
22340
|
const environment = normaliseEnvironment(args.environment);
|
|
22015
22341
|
try {
|
|
@@ -22120,7 +22446,7 @@ ${versionDrift}` : "";
|
|
|
22120
22446
|
let unregisteredDocsNote = "";
|
|
22121
22447
|
try {
|
|
22122
22448
|
if (adapter2.searchDocs) {
|
|
22123
|
-
const docsDir =
|
|
22449
|
+
const docsDir = join11(config2.projectRoot, "docs");
|
|
22124
22450
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22125
22451
|
if (docsFiles.length > 0) {
|
|
22126
22452
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -22171,7 +22497,7 @@ ${versionDrift}` : "";
|
|
|
22171
22497
|
lines.push(` \u2192 \u2026and ${pendingActions.length - 2} more`);
|
|
22172
22498
|
}
|
|
22173
22499
|
}
|
|
22174
|
-
lines.push("
|
|
22500
|
+
lines.push("_Promote an actionable item now via `doc_action_promote`, or factor into next `strategy_review`. Run `doc_search` for full details._");
|
|
22175
22501
|
researchSignalsNote = lines.join("\n");
|
|
22176
22502
|
}
|
|
22177
22503
|
}
|
|
@@ -22215,7 +22541,6 @@ ${versionDrift}` : "";
|
|
|
22215
22541
|
try {
|
|
22216
22542
|
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
22217
22543
|
if (learnings) {
|
|
22218
|
-
const byRecency = (a, b2) => (b2.createdAt ?? "").localeCompare(a.createdAt ?? "");
|
|
22219
22544
|
const candidateLearnings = learnings.filter((l) => !l.actionTaken);
|
|
22220
22545
|
const referencedTaskIds = Array.from(new Set(candidateLearnings.map((l) => l.taskId).filter(Boolean)));
|
|
22221
22546
|
let closedTaskIds = /* @__PURE__ */ new Set();
|
|
@@ -22228,35 +22553,26 @@ ${versionDrift}` : "";
|
|
|
22228
22553
|
} catch {
|
|
22229
22554
|
}
|
|
22230
22555
|
}
|
|
22231
|
-
const
|
|
22232
|
-
|
|
22233
|
-
|
|
22234
|
-
|
|
22235
|
-
|
|
22236
|
-
|
|
22237
|
-
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
22247
|
-
|
|
22248
|
-
|
|
22249
|
-
|
|
22250
|
-
|
|
22251
|
-
const totalLow = totalP2 + totalP3;
|
|
22252
|
-
const header = totalLow > UNACTIONED_CAP ? `${totalP2} P2 \xB7 ${totalP3} P3 (showing ${UNACTIONED_CAP} most recent)` : `${totalP2} P2 \xB7 ${totalP3} P3`;
|
|
22253
|
-
const lines = ["\n\n## Unactioned Issues", header];
|
|
22254
|
-
for (const issue of unactioned) {
|
|
22255
|
-
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
22256
|
-
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
22257
|
-
}
|
|
22258
|
-
lines.push("_Run `idea` to log these as backlog tasks, or `board_edit` if already handled._");
|
|
22259
|
-
unactionedIssuesNote = lines.join("\n");
|
|
22556
|
+
const formatted = formatDiscoveredIssuesBlocks(candidateLearnings, closedTaskIds);
|
|
22557
|
+
alertsNote = formatted.alertsNote;
|
|
22558
|
+
unactionedIssuesNote = formatted.unactionedIssuesNote;
|
|
22559
|
+
}
|
|
22560
|
+
} catch {
|
|
22561
|
+
}
|
|
22562
|
+
let skillProposalsNote = "";
|
|
22563
|
+
try {
|
|
22564
|
+
const metrics = await adapter2.readToolMetrics();
|
|
22565
|
+
const alreadyScanned = metrics.some((m) => m.tool === "milestone:skill_scan_completed");
|
|
22566
|
+
if (!alreadyScanned) {
|
|
22567
|
+
const proposals = scanForSkillSignals(config2.projectRoot);
|
|
22568
|
+
skillProposalsNote = formatSkillProposals(proposals);
|
|
22569
|
+
try {
|
|
22570
|
+
await adapter2.appendToolMetric({
|
|
22571
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22572
|
+
tool: "milestone:skill_scan_completed",
|
|
22573
|
+
durationMs: 0
|
|
22574
|
+
});
|
|
22575
|
+
} catch {
|
|
22260
22576
|
}
|
|
22261
22577
|
}
|
|
22262
22578
|
} catch {
|
|
@@ -22272,16 +22588,16 @@ ${versionDrift}` : "";
|
|
|
22272
22588
|
markOrient();
|
|
22273
22589
|
} catch {
|
|
22274
22590
|
}
|
|
22275
|
-
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment) + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + sessionGuidanceNote + versionNote + enrichmentNote);
|
|
22591
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot, environment) + alertsNote + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + researchSignalsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + skillProposalsNote + sessionGuidanceNote + versionNote + enrichmentNote);
|
|
22276
22592
|
} catch (err) {
|
|
22277
22593
|
const message = err instanceof Error ? err.message : String(err);
|
|
22278
22594
|
return errorResponse(`Orient failed: ${message}`);
|
|
22279
22595
|
}
|
|
22280
22596
|
}
|
|
22281
22597
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
22282
|
-
const claudeMdPath =
|
|
22283
|
-
if (!
|
|
22284
|
-
const content =
|
|
22598
|
+
const claudeMdPath = join11(projectRoot, "CLAUDE.md");
|
|
22599
|
+
if (!existsSync8(claudeMdPath)) return "";
|
|
22600
|
+
const content = readFileSync4(claudeMdPath, "utf-8");
|
|
22285
22601
|
const additions = [];
|
|
22286
22602
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
22287
22603
|
additions.push(CLAUDE_MD_TIER_1);
|
|
@@ -23063,12 +23379,12 @@ ${result.userMessage}
|
|
|
23063
23379
|
}
|
|
23064
23380
|
|
|
23065
23381
|
// src/lib/install-id.ts
|
|
23066
|
-
import { randomUUID as
|
|
23067
|
-
import { mkdirSync, readFileSync as
|
|
23382
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
23383
|
+
import { mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync } from "fs";
|
|
23068
23384
|
import { homedir as homedir3 } from "os";
|
|
23069
|
-
import { join as
|
|
23070
|
-
var PAPI_HOME_DIR =
|
|
23071
|
-
var INSTALL_ID_FILE =
|
|
23385
|
+
import { join as join12 } from "path";
|
|
23386
|
+
var PAPI_HOME_DIR = join12(homedir3(), ".papi");
|
|
23387
|
+
var INSTALL_ID_FILE = join12(PAPI_HOME_DIR, "install-id.json");
|
|
23072
23388
|
var cachedInstallId = null;
|
|
23073
23389
|
function isValidUuid(s) {
|
|
23074
23390
|
return typeof s === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
|
@@ -23076,7 +23392,7 @@ function isValidUuid(s) {
|
|
|
23076
23392
|
function getInstallId() {
|
|
23077
23393
|
if (cachedInstallId) return cachedInstallId;
|
|
23078
23394
|
try {
|
|
23079
|
-
const raw =
|
|
23395
|
+
const raw = readFileSync5(INSTALL_ID_FILE, "utf-8");
|
|
23080
23396
|
const parsed = JSON.parse(raw);
|
|
23081
23397
|
if (isValidUuid(parsed.install_id)) {
|
|
23082
23398
|
cachedInstallId = parsed.install_id;
|
|
@@ -23086,7 +23402,7 @@ function getInstallId() {
|
|
|
23086
23402
|
}
|
|
23087
23403
|
try {
|
|
23088
23404
|
mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
|
|
23089
|
-
const id =
|
|
23405
|
+
const id = randomUUID16();
|
|
23090
23406
|
const contents = {
|
|
23091
23407
|
install_id: id,
|
|
23092
23408
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -23206,7 +23522,7 @@ function createServer(adapter2, config2) {
|
|
|
23206
23522
|
const __pkgDir = dirname(__pkgFilename);
|
|
23207
23523
|
let serverVersion = "unknown";
|
|
23208
23524
|
try {
|
|
23209
|
-
const pkg = JSON.parse(
|
|
23525
|
+
const pkg = JSON.parse(readFileSync6(join13(__pkgDir, "..", "package.json"), "utf-8"));
|
|
23210
23526
|
serverVersion = pkg.version ?? "unknown";
|
|
23211
23527
|
} catch {
|
|
23212
23528
|
}
|
|
@@ -23221,7 +23537,7 @@ function createServer(adapter2, config2) {
|
|
|
23221
23537
|
}
|
|
23222
23538
|
const __filename = fileURLToPath(import.meta.url);
|
|
23223
23539
|
const __dirname2 = dirname(__filename);
|
|
23224
|
-
const skillsDir =
|
|
23540
|
+
const skillsDir = join13(__dirname2, "..", "skills");
|
|
23225
23541
|
function parseSkillFrontmatter(content) {
|
|
23226
23542
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
23227
23543
|
if (!match) return null;
|
|
@@ -23239,7 +23555,7 @@ function createServer(adapter2, config2) {
|
|
|
23239
23555
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
23240
23556
|
const prompts = [];
|
|
23241
23557
|
for (const file of mdFiles) {
|
|
23242
|
-
const content = await readFile5(
|
|
23558
|
+
const content = await readFile5(join13(skillsDir, file), "utf-8");
|
|
23243
23559
|
const meta = parseSkillFrontmatter(content);
|
|
23244
23560
|
if (meta) {
|
|
23245
23561
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -23255,7 +23571,7 @@ function createServer(adapter2, config2) {
|
|
|
23255
23571
|
try {
|
|
23256
23572
|
const files = await readdir2(skillsDir);
|
|
23257
23573
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
23258
|
-
const content = await readFile5(
|
|
23574
|
+
const content = await readFile5(join13(skillsDir, file), "utf-8");
|
|
23259
23575
|
const meta = parseSkillFrontmatter(content);
|
|
23260
23576
|
if (meta?.name === name) {
|
|
23261
23577
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -23298,6 +23614,7 @@ function createServer(adapter2, config2) {
|
|
|
23298
23614
|
docRegisterTool,
|
|
23299
23615
|
docSearchTool,
|
|
23300
23616
|
docScanTool,
|
|
23617
|
+
docActionPromoteTool,
|
|
23301
23618
|
getSiblingAdsTool,
|
|
23302
23619
|
handoffGenerateTool
|
|
23303
23620
|
]
|
|
@@ -23413,6 +23730,9 @@ function createServer(adapter2, config2) {
|
|
|
23413
23730
|
case "doc_scan":
|
|
23414
23731
|
result = await handleDocScan(adapter2, config2, safeArgs);
|
|
23415
23732
|
break;
|
|
23733
|
+
case "doc_action_promote":
|
|
23734
|
+
result = await handleDocActionPromote(adapter2, safeArgs);
|
|
23735
|
+
break;
|
|
23416
23736
|
case "get_sibling_ads":
|
|
23417
23737
|
result = await handleGetSiblingAds(adapter2, safeArgs);
|
|
23418
23738
|
break;
|
|
@@ -23468,7 +23788,7 @@ function createServer(adapter2, config2) {
|
|
|
23468
23788
|
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
23469
23789
|
var pkgVersion = "unknown";
|
|
23470
23790
|
try {
|
|
23471
|
-
const pkg = JSON.parse(
|
|
23791
|
+
const pkg = JSON.parse(readFileSync7(join14(__dirname, "..", "package.json"), "utf-8"));
|
|
23472
23792
|
pkgVersion = pkg.version;
|
|
23473
23793
|
} catch {
|
|
23474
23794
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -193,6 +193,7 @@ Standard planning cycle with full board review.
|
|
|
193
193
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
194
194
|
**\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.
|
|
195
195
|
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.
|
|
196
|
+
**Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
|
|
196
197
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
197
198
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
198
199
|
|
|
@@ -431,6 +432,7 @@ Standard planning cycle with full board review.
|
|
|
431
432
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
432
433
|
**\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.
|
|
433
434
|
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.
|
|
435
|
+
**Module classification for cross-cutting tasks:** When a task title contains "audit"/"unfiltered"/"scoping"/"leak" plus a database-entity name (e.g. "audit ... cycle_learnings reads", "unfiltered cycle_tasks queries"), classify the module by the actual code surface that reads/writes the entity \u2014 not by the tool names mentioned in the title. The reasoning surface (e.g. "health" or "strategy_review") is often unrelated to the data-access surface. Resolve this by treating the entity name as the routing signal: tasks touching dashboard read/write paths belong to the Dashboard module even if the title mentions an MCP tool. Misclassification routes the task to the wrong shared cycle branch and surfaces the wrong MODULE INSTRUCTIONS to the builder.
|
|
434
436
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
435
437
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
436
438
|
|
|
@@ -645,6 +647,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
645
647
|
if (ctx.registeredDocs) {
|
|
646
648
|
parts.push("### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
647
649
|
}
|
|
650
|
+
if (ctx.unactionedDocs) {
|
|
651
|
+
parts.push(
|
|
652
|
+
"### Unactioned Research (soft-warn)",
|
|
653
|
+
"",
|
|
654
|
+
"These registered docs still have pending actions. Before adding net-new tasks, consider whether one of these unactioned items should be promoted into this cycle via `doc_action_promote`. Surface this in your plan output so the user can decide.",
|
|
655
|
+
"",
|
|
656
|
+
ctx.unactionedDocs,
|
|
657
|
+
""
|
|
658
|
+
);
|
|
659
|
+
}
|
|
648
660
|
if (ctx.carryForwardStaleness) {
|
|
649
661
|
parts.push("### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
650
662
|
}
|
|
@@ -857,6 +869,13 @@ ${AD_REJECTION_RULES}
|
|
|
857
869
|
|
|
858
870
|
**Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
|
|
859
871
|
|
|
872
|
+
**Doc Action Staleness:** If a "### Doc Action Staleness" section is present, treat it as a research-to-action audit. For each entry:
|
|
873
|
+
- **Stale** (no Done task, >20 cycles old): explicitly call it out in section 2 (Product Gaps) or 3 (Opportunities) \u2014 research that's been sitting unactioned this long is either (a) no longer relevant (recommend marking the doc archived) or (b) silently dropped (recommend promoting via \`doc_action_promote\` into the next plan).
|
|
874
|
+
- **Completed but not closed** (linked task is Done): instruct in your review that the action should be marked resolved \u2014 it's done work the registry hasn't caught up to.
|
|
875
|
+
- **Deferred** (<20 cycles, no matching Done task): only mention if it relates to a current strategic theme; otherwise leave it.
|
|
876
|
+
|
|
877
|
+
For every stale or unresolved item you call out, propose a concrete next step (promote, archive, or supersede) \u2014 don't just observe.
|
|
878
|
+
|
|
860
879
|
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
861
880
|
|
|
862
881
|
6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
|
package/package.json
CHANGED