@papi-ai/server 0.7.13 → 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 +523 -106
- package/dist/prompts.js +44 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,12 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// ../shared/dist/index.js
|
|
13
|
-
|
|
13
|
+
function isLiveDecision(d) {
|
|
14
|
+
if (d.superseded === true) return false;
|
|
15
|
+
if (d.outcome != null && RETIRED_DECISION_OUTCOMES.includes(d.outcome)) return false;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
var VALID_TRANSITIONS, RETIRED_DECISION_OUTCOMES;
|
|
14
19
|
var init_dist = __esm({
|
|
15
20
|
"../shared/dist/index.js"() {
|
|
16
21
|
"use strict";
|
|
@@ -25,6 +30,7 @@ var init_dist = __esm({
|
|
|
25
30
|
"Cancelled": [],
|
|
26
31
|
"Deferred": ["Backlog", "Cancelled"]
|
|
27
32
|
};
|
|
33
|
+
RETIRED_DECISION_OUTCOMES = ["resolved", "abandoned", "superseded"];
|
|
28
34
|
}
|
|
29
35
|
});
|
|
30
36
|
|
|
@@ -1441,12 +1447,13 @@ async function detectReviewPatterns(reviews, currentCycle, window = 5, clusterer
|
|
|
1441
1447
|
function hasReviewPatterns(patterns) {
|
|
1442
1448
|
return patterns.recurringFeedback.length > 0 || patterns.requestChangesRate >= 50;
|
|
1443
1449
|
}
|
|
1444
|
-
var VALID_TRANSITIONS2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
|
|
1450
|
+
var VALID_TRANSITIONS2, isLiveDecision2, TASK_TYPE_TIERS, VALID_EFFORT_SIZES, SECTION_HEADERS, YAML_MARKER, YAML_START, YAML_END, VALID_EFFORT_SIZES2, HEADER_SENTINEL, TABLE_HEADER, TABLE_SEPARATOR, PREV_TABLE_HEADER, LEGACY_TABLE_HEADER, SECTION_HEADING, FILE_TEMPLATE, COST_SECTION_HEADING, COST_TABLE_SEPARATOR, FILE_HEADING, ACCURACY_HEADER, ACCURACY_SEPARATOR, VELOCITY_HEADER, VELOCITY_SEPARATOR, EFFORT_SCALE, NONE_PATTERN, HEADER_SENTINEL2, VALID_STAGES, VALID_VERDICTS, STAGE_DISPLAY, VALID_STATUSES, PHASES_START, PHASES_END, YAML_MARKER2, YAML_START2, YAML_END2, VALID_STATUSES2, YAML_MARKER3, YAML_START3, YAML_END3, MdFileAdapter, NONE_PATTERN2;
|
|
1445
1451
|
var init_dist2 = __esm({
|
|
1446
1452
|
"../adapter-md/dist/index.js"() {
|
|
1447
1453
|
"use strict";
|
|
1448
1454
|
init_dist();
|
|
1449
1455
|
VALID_TRANSITIONS2 = VALID_TRANSITIONS;
|
|
1456
|
+
isLiveDecision2 = isLiveDecision;
|
|
1450
1457
|
TASK_TYPE_TIERS = {
|
|
1451
1458
|
bug: 1,
|
|
1452
1459
|
task: 1,
|
|
@@ -1557,11 +1564,18 @@ ${TABLE_SEPARATOR}
|
|
|
1557
1564
|
async getCycleHealth() {
|
|
1558
1565
|
return parseCycleHealth(await this.read("PLANNING_LOG.md"));
|
|
1559
1566
|
}
|
|
1560
|
-
/**
|
|
1561
|
-
|
|
1567
|
+
/**
|
|
1568
|
+
* Read Active Decisions from ACTIVE_DECISIONS.md.
|
|
1569
|
+
*
|
|
1570
|
+
* Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
|
|
1571
|
+
* Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
|
|
1572
|
+
*/
|
|
1573
|
+
async getActiveDecisions(options) {
|
|
1562
1574
|
const content = await this.readOptional("ACTIVE_DECISIONS.md");
|
|
1563
1575
|
if (!content) return [];
|
|
1564
|
-
|
|
1576
|
+
const all = parseActiveDecisions(content);
|
|
1577
|
+
if (options?.includeRetired) return all;
|
|
1578
|
+
return all.filter(isLiveDecision2);
|
|
1565
1579
|
}
|
|
1566
1580
|
/** Read cycle log entries (newest first), optionally limited to {@link limit} entries. */
|
|
1567
1581
|
async getCycleLog(limit) {
|
|
@@ -6352,11 +6366,31 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6352
6366
|
lastFullMode: 0
|
|
6353
6367
|
};
|
|
6354
6368
|
}
|
|
6355
|
-
|
|
6369
|
+
/**
|
|
6370
|
+
* Read Active Decisions for this project.
|
|
6371
|
+
*
|
|
6372
|
+
* Default filters out retired ADs (outcome ∈ abandoned/superseded/resolved or superseded=true).
|
|
6373
|
+
* Pass { includeRetired: true } for management/triage surfaces. See PapiAdapter docstring.
|
|
6374
|
+
*
|
|
6375
|
+
* task-1546 (C242 hot-fix): closes the bug where retired ADs leaked into context surfaces.
|
|
6376
|
+
*/
|
|
6377
|
+
async getActiveDecisions(options) {
|
|
6378
|
+
if (options?.includeRetired) {
|
|
6379
|
+
const rows2 = await this.sql`
|
|
6380
|
+
SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
|
|
6381
|
+
FROM active_decisions
|
|
6382
|
+
WHERE project_id = ${this.projectId}
|
|
6383
|
+
ORDER BY display_id
|
|
6384
|
+
LIMIT 200
|
|
6385
|
+
`;
|
|
6386
|
+
return rows2.map(rowToActiveDecision);
|
|
6387
|
+
}
|
|
6356
6388
|
const rows = await this.sql`
|
|
6357
6389
|
SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
|
|
6358
6390
|
FROM active_decisions
|
|
6359
6391
|
WHERE project_id = ${this.projectId}
|
|
6392
|
+
AND superseded = false
|
|
6393
|
+
AND (outcome IS NULL OR outcome NOT IN ('abandoned', 'superseded', 'resolved'))
|
|
6360
6394
|
ORDER BY display_id
|
|
6361
6395
|
LIMIT 200 -- bounded: ADs are bounded by project lifecycle, 200 is a safe ceiling
|
|
6362
6396
|
`;
|
|
@@ -6699,6 +6733,25 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6699
6733
|
superseded_by = ${supersededBy ?? null},
|
|
6700
6734
|
updated_at = now()
|
|
6701
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}
|
|
6702
6755
|
`;
|
|
6703
6756
|
}
|
|
6704
6757
|
async writeDogfoodEntries(entries) {
|
|
@@ -7573,7 +7626,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7573
7626
|
const rows = await this.sql`
|
|
7574
7627
|
SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
|
|
7575
7628
|
FROM stages
|
|
7576
|
-
WHERE project_id = ${this.projectId} AND status = '
|
|
7629
|
+
WHERE project_id = ${this.projectId} AND status = 'In Progress'
|
|
7577
7630
|
ORDER BY sort_order
|
|
7578
7631
|
LIMIT 1
|
|
7579
7632
|
`;
|
|
@@ -8575,8 +8628,8 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8575
8628
|
getCycleHealth() {
|
|
8576
8629
|
return this.invoke("getCycleHealth");
|
|
8577
8630
|
}
|
|
8578
|
-
getActiveDecisions() {
|
|
8579
|
-
return this.invoke("getActiveDecisions");
|
|
8631
|
+
getActiveDecisions(options) {
|
|
8632
|
+
return this.invoke("getActiveDecisions", [options ?? {}]);
|
|
8580
8633
|
}
|
|
8581
8634
|
getCycleLog(limit) {
|
|
8582
8635
|
return this.invoke("getCycleLog", [limit]);
|
|
@@ -9452,9 +9505,9 @@ var init_git = __esm({
|
|
|
9452
9505
|
});
|
|
9453
9506
|
|
|
9454
9507
|
// src/index.ts
|
|
9455
|
-
import { readFileSync as
|
|
9508
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
9456
9509
|
import { createServer as createHttpServer } from "http";
|
|
9457
|
-
import { dirname as dirname2, join as
|
|
9510
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
9458
9511
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9459
9512
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9460
9513
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -9741,9 +9794,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
9741
9794
|
}
|
|
9742
9795
|
|
|
9743
9796
|
// src/server.ts
|
|
9744
|
-
import { readFileSync as
|
|
9797
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
9745
9798
|
import { access as access4, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
9746
|
-
import { join as
|
|
9799
|
+
import { join as join13, dirname } from "path";
|
|
9747
9800
|
import { fileURLToPath } from "url";
|
|
9748
9801
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9749
9802
|
import {
|
|
@@ -10535,6 +10588,20 @@ async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Rev
|
|
|
10535
10588
|
}
|
|
10536
10589
|
|
|
10537
10590
|
// src/prompts.ts
|
|
10591
|
+
var AD_REJECTION_RULES = `**AD Minting Guard \u2014 REJECT observations dressed as decisions.**
|
|
10592
|
+
|
|
10593
|
+
An Active Decision expresses a *stance* the project is taking \u2014 a choice between alternatives that constrains future work. Reject any candidate AD whose body asserts:
|
|
10594
|
+
(a) the existence, identity, or status of a person, user, or external entity (e.g. "User X is building Y", "Customer Z is active");
|
|
10595
|
+
(b) a metric or measurement (e.g. "Signups grew 3x last cycle", "Latency dropped to 200ms");
|
|
10596
|
+
(c) a fact about the current state of the world that could be confirmed or denied by a query rather than challenged by argument (e.g. "External user feedback is now flowing", "The /admin route exists").
|
|
10597
|
+
|
|
10598
|
+
If a candidate AD body could be invalidated by running a SQL query, refreshing a dashboard, or checking a log \u2014 it is an observation, not a decision. Capture it as a build report finding, a dogfood observation, a cycle log note, or a registered doc instead. Do NOT mint it as an AD.
|
|
10599
|
+
|
|
10600
|
+
**Positive example (valid AD):** "Pricing tier strategy: free engine + paid intelligence. Decision: keep cycles free, charge for strategy reviews and analytics. Why: telemetry shows engagement clusters around intelligence surfaces, not engine surfaces." \u2014 this is a stance with alternatives.
|
|
10601
|
+
|
|
10602
|
+
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
10603
|
+
|
|
10604
|
+
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
10538
10605
|
var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
|
|
10539
10606
|
You receive project context and produce a planning cycle output with a BUILD HANDOFF.
|
|
10540
10607
|
|
|
@@ -10715,6 +10782,7 @@ Standard planning cycle with full board review.
|
|
|
10715
10782
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10716
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.
|
|
10717
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.
|
|
10718
10786
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
10719
10787
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
10720
10788
|
|
|
@@ -10760,6 +10828,9 @@ Standard planning cycle with full board review.
|
|
|
10760
10828
|
|
|
10761
10829
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
10762
10830
|
**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.
|
|
10831
|
+
|
|
10832
|
+
${AD_REJECTION_RULES}
|
|
10833
|
+
|
|
10763
10834
|
**\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.
|
|
10764
10835
|
|
|
10765
10836
|
### Operational Quality Rules
|
|
@@ -10950,6 +11021,7 @@ Standard planning cycle with full board review.
|
|
|
10950
11021
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10951
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.
|
|
10952
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.
|
|
10953
11025
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
10954
11026
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
10955
11027
|
|
|
@@ -10997,6 +11069,9 @@ Standard planning cycle with full board review.
|
|
|
10997
11069
|
|
|
10998
11070
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
10999
11071
|
**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.
|
|
11072
|
+
|
|
11073
|
+
${AD_REJECTION_RULES}
|
|
11074
|
+
|
|
11000
11075
|
**\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.
|
|
11001
11076
|
|
|
11002
11077
|
### Operational Quality Rules
|
|
@@ -11161,6 +11236,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
11161
11236
|
if (ctx.registeredDocs) {
|
|
11162
11237
|
parts.push("### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
11163
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
|
+
}
|
|
11164
11249
|
if (ctx.carryForwardStaleness) {
|
|
11165
11250
|
parts.push("### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
11166
11251
|
}
|
|
@@ -11369,8 +11454,17 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
11369
11454
|
- Note any hierarchy/phase issues worth correcting (1-2 bullets max)
|
|
11370
11455
|
- Delete ADs that are legacy, process-level, or redundant without discussion
|
|
11371
11456
|
|
|
11457
|
+
${AD_REJECTION_RULES}
|
|
11458
|
+
|
|
11372
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.
|
|
11373
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
|
+
|
|
11374
11468
|
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
11375
11469
|
|
|
11376
11470
|
6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
|
|
@@ -11678,6 +11772,8 @@ The JSON must be valid. Only include ADs that need changes \u2014 omit unchanged
|
|
|
11678
11772
|
For new ADs, use the next available AD number.
|
|
11679
11773
|
The body field must be the COMPLETE replacement text for the AD block (including the ### heading line).
|
|
11680
11774
|
|
|
11775
|
+
${AD_REJECTION_RULES}
|
|
11776
|
+
|
|
11681
11777
|
## PHASE UPDATES
|
|
11682
11778
|
|
|
11683
11779
|
If the strategic change affects the project's phase structure, include a phaseUpdates array.
|
|
@@ -12050,6 +12146,7 @@ function applyContextTier(ctx, cycleCount) {
|
|
|
12050
12146
|
ctx.reviewPatterns = void 0;
|
|
12051
12147
|
ctx.horizonContext = void 0;
|
|
12052
12148
|
ctx.registeredDocs = void 0;
|
|
12149
|
+
ctx.unactionedDocs = void 0;
|
|
12053
12150
|
ctx.recentReviews = void 0;
|
|
12054
12151
|
ctx.strategyReviewCadence = void 0;
|
|
12055
12152
|
}
|
|
@@ -12219,6 +12316,32 @@ function formatStrategyRecommendations(recs, currentCycle) {
|
|
|
12219
12316
|
}
|
|
12220
12317
|
return sections.join("\n");
|
|
12221
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
|
+
}
|
|
12222
12345
|
function formatEstimationCalibration(rows) {
|
|
12223
12346
|
const total = rows.reduce((sum, r) => sum + r.count, 0);
|
|
12224
12347
|
const accurate = rows.filter((r) => r.accuracyLabel === "accurate").reduce((sum, r) => sum + r.count, 0);
|
|
@@ -12558,11 +12681,13 @@ async function assembleContext(adapter2, mode, _config, filters, focus) {
|
|
|
12558
12681
|
}
|
|
12559
12682
|
}
|
|
12560
12683
|
let registeredDocsText;
|
|
12684
|
+
let unactionedDocsTextLean;
|
|
12561
12685
|
if (docsResult.status === "fulfilled" && docsResult.value && docsResult.value.length > 0) {
|
|
12562
12686
|
const docs = docsResult.value;
|
|
12563
12687
|
const lines = docs.map((d) => `- **${d.title}** (${d.type}) \u2014 ${d.summary}`);
|
|
12564
12688
|
registeredDocsText = `${docs.length} active research doc(s):
|
|
12565
12689
|
${lines.join("\n")}`;
|
|
12690
|
+
unactionedDocsTextLean = formatUnactionedDocs(docs);
|
|
12566
12691
|
}
|
|
12567
12692
|
const boardFlags = detectBoardFlagsFromText(leanSummary.board);
|
|
12568
12693
|
let carryForwardStalenessLean;
|
|
@@ -12614,6 +12739,7 @@ ${lines.join("\n")}`;
|
|
|
12614
12739
|
discoveryCanvas: discoveryCanvasText,
|
|
12615
12740
|
estimationCalibration: estimationCalibrationText,
|
|
12616
12741
|
registeredDocs: registeredDocsText,
|
|
12742
|
+
unactionedDocs: unactionedDocsTextLean,
|
|
12617
12743
|
focus,
|
|
12618
12744
|
boardFlags,
|
|
12619
12745
|
carryForwardStaleness: carryForwardStalenessLean,
|
|
@@ -12691,9 +12817,24 @@ ${lines.join("\n")}`;
|
|
|
12691
12817
|
const metricsSnapshots = filteredRaw.length > 0 ? filteredRaw : computeSnapshotsFromBuildReports(allReportsForPatterns);
|
|
12692
12818
|
const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
|
|
12693
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
|
+
}
|
|
12694
12833
|
let registeredDocsTextFull;
|
|
12834
|
+
let unactionedDocsTextFull;
|
|
12695
12835
|
if (docsResultFull.status === "fulfilled" && docsResultFull.value && docsResultFull.value.length > 0) {
|
|
12696
12836
|
const allDocs = docsResultFull.value;
|
|
12837
|
+
unactionedDocsTextFull = formatUnactionedDocs(allDocs);
|
|
12697
12838
|
const taskModules = new Set(plannerTasks.map((t2) => t2.module?.toLowerCase()).filter(Boolean));
|
|
12698
12839
|
const taskEpics = new Set(plannerTasks.map((t2) => t2.epic?.toLowerCase()).filter(Boolean));
|
|
12699
12840
|
const HIGH_VALUE_TYPES = /* @__PURE__ */ new Set(["architecture", "guide", "research"]);
|
|
@@ -12739,19 +12880,6 @@ ${logLines}`);
|
|
|
12739
12880
|
const filtered = reports.filter((r) => (r.cycle ?? 0) >= cutoff);
|
|
12740
12881
|
return filtered.length > 0 ? filtered : reports.slice(0, 5);
|
|
12741
12882
|
})() : reports;
|
|
12742
|
-
const strippedTasks = stripTasksForPlan(tasks);
|
|
12743
|
-
const boardFlagsFull = detectBoardFlags(tasks);
|
|
12744
|
-
const horizonCtx = buildHorizonContext(phases, tasks) ?? void 0;
|
|
12745
|
-
const ACTIVE_STATUSES2 = /* @__PURE__ */ new Set(["In Progress", "In Review", "Blocked"]);
|
|
12746
|
-
const p3Excluded = strippedTasks.filter(
|
|
12747
|
-
(t2) => t2.priority === "P3 Low" && !ACTIVE_STATUSES2.has(t2.status)
|
|
12748
|
-
);
|
|
12749
|
-
const plannerTasks = strippedTasks.filter(
|
|
12750
|
-
(t2) => t2.priority !== "P3 Low" || ACTIVE_STATUSES2.has(t2.status)
|
|
12751
|
-
);
|
|
12752
|
-
if (p3Excluded.length > 0) {
|
|
12753
|
-
console.error(`[plan-perf] board tiering: excluded ${p3Excluded.length} P3 Low tasks from planner context`);
|
|
12754
|
-
}
|
|
12755
12883
|
const targetCycle = health.totalCycles + 1;
|
|
12756
12884
|
const preAssigned = plannerTasks.filter((t2) => t2.cycle === targetCycle);
|
|
12757
12885
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
@@ -12777,6 +12905,7 @@ ${logLines}`);
|
|
|
12777
12905
|
taskComments: taskCommentsTextFull,
|
|
12778
12906
|
discoveryCanvas: discoveryCanvasTextFull,
|
|
12779
12907
|
registeredDocs: registeredDocsTextFull,
|
|
12908
|
+
unactionedDocs: unactionedDocsTextFull,
|
|
12780
12909
|
focus,
|
|
12781
12910
|
boardFlags: boardFlagsFull,
|
|
12782
12911
|
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
@@ -13736,17 +13865,19 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
13736
13865
|
const expectedCycleNumber = lastPrepareCycleNumber;
|
|
13737
13866
|
const skipHandoffsCached = lastPrepareSkipHandoffs;
|
|
13738
13867
|
const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
|
|
13739
|
-
const
|
|
13740
|
-
|
|
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)) {
|
|
13741
13871
|
return errorResponse(
|
|
13742
13872
|
"cycle_number is required for apply mode. Pass the cycle_number from the prepare phase output."
|
|
13743
13873
|
);
|
|
13744
13874
|
}
|
|
13745
|
-
if (
|
|
13875
|
+
if (expectedNewCycleNumber !== void 0 && newCycleNumber !== expectedNewCycleNumber) {
|
|
13746
13876
|
return errorResponse(
|
|
13747
|
-
`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.`
|
|
13748
13878
|
);
|
|
13749
13879
|
}
|
|
13880
|
+
const cycleNumber = newCycleNumber - 1;
|
|
13750
13881
|
lastPrepareContextHashes = void 0;
|
|
13751
13882
|
lastPrepareUserMessage = void 0;
|
|
13752
13883
|
lastPrepareContextBytes = void 0;
|
|
@@ -13800,7 +13931,7 @@ When done, call \`plan\` again with:
|
|
|
13800
13931
|
- \`mode\`: "apply"
|
|
13801
13932
|
- \`llm_response\`: your complete output (both parts)
|
|
13802
13933
|
- \`plan_mode\`: "${result.mode}"
|
|
13803
|
-
- \`cycle_number\`: ${result.cycleNumber}
|
|
13934
|
+
- \`cycle_number\`: ${result.cycleNumber + 1}
|
|
13804
13935
|
- \`strategy_review_warning\`: "${result.strategyReviewWarning.replace(/"/g, '\\"')}"
|
|
13805
13936
|
|
|
13806
13937
|
---
|
|
@@ -14140,7 +14271,8 @@ async function assembleContext2(adapter2, cycleNumber, cyclesSinceLastReview, pr
|
|
|
14140
14271
|
docsWithPendingActions
|
|
14141
14272
|
] = await Promise.all([
|
|
14142
14273
|
adapter2.readProductBrief(),
|
|
14143
|
-
|
|
14274
|
+
// Strategy review needs to see retired ADs to triage/restore them as needed.
|
|
14275
|
+
adapter2.getActiveDecisions({ includeRetired: true }),
|
|
14144
14276
|
adapter2.getBuildReportsSince(lastReviewCycleNum),
|
|
14145
14277
|
adapter2.getCycleLogSince(lastReviewCycleNum),
|
|
14146
14278
|
adapter2.queryBoard({
|
|
@@ -14642,7 +14774,7 @@ ${cleanContent}`;
|
|
|
14642
14774
|
try {
|
|
14643
14775
|
const recs = extractRecommendations(data, cycleNumber);
|
|
14644
14776
|
if (recs.length > 0) {
|
|
14645
|
-
const existingAds = await adapter2.getActiveDecisions().catch(() => []);
|
|
14777
|
+
const existingAds = await adapter2.getActiveDecisions({ includeRetired: true }).catch(() => []);
|
|
14646
14778
|
const existingAdIds = new Set(existingAds.map((ad) => ad.id));
|
|
14647
14779
|
const filteredRecs = recs.filter((rec) => {
|
|
14648
14780
|
if (rec.target && /^AD-\d+$/.test(rec.target)) {
|
|
@@ -15176,7 +15308,8 @@ async function prepareStrategyChange(adapter2, text) {
|
|
|
15176
15308
|
try {
|
|
15177
15309
|
const [brief, decisions, readPhases, boardTasks, reports, previousReviews] = await Promise.all([
|
|
15178
15310
|
adapter2.readProductBrief(),
|
|
15179
|
-
|
|
15311
|
+
// Strategy review needs ALL ADs (live + retired) for housekeeping.
|
|
15312
|
+
adapter2.getActiveDecisions({ includeRetired: true }),
|
|
15180
15313
|
adapter2.readPhases(),
|
|
15181
15314
|
adapter2.queryBoard().catch(() => []),
|
|
15182
15315
|
adapter2.getRecentBuildReports(15).catch(() => []),
|
|
@@ -15221,7 +15354,7 @@ async function captureDecision(adapter2, input) {
|
|
|
15221
15354
|
adId = input.adId;
|
|
15222
15355
|
adAction = "updated";
|
|
15223
15356
|
} else {
|
|
15224
|
-
const existingAds = await adapter2.getActiveDecisions();
|
|
15357
|
+
const existingAds = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
15225
15358
|
const maxNum = existingAds.reduce((max, ad) => {
|
|
15226
15359
|
const match = ad.id.match(/^AD-(\d+)$/);
|
|
15227
15360
|
return match ? Math.max(max, parseInt(match[1], 10)) : max;
|
|
@@ -17638,6 +17771,23 @@ function isNoHandoffError(err) {
|
|
|
17638
17771
|
function isBlockedError(err) {
|
|
17639
17772
|
return err instanceof Error && err.code === "BLOCKED";
|
|
17640
17773
|
}
|
|
17774
|
+
function computeScopeDriftSignal(predicted, changed) {
|
|
17775
|
+
if (!predicted || predicted.length === 0) return null;
|
|
17776
|
+
if (changed.length === 0) return null;
|
|
17777
|
+
const basename2 = (p) => {
|
|
17778
|
+
const parts = p.split(/[\\/]/);
|
|
17779
|
+
return parts[parts.length - 1] ?? p;
|
|
17780
|
+
};
|
|
17781
|
+
const predictedNames = new Set(predicted.map(basename2).filter(Boolean));
|
|
17782
|
+
const unexpected = changed.filter((c) => !predictedNames.has(basename2(c)));
|
|
17783
|
+
const fraction = unexpected.length / changed.length;
|
|
17784
|
+
const triggered = fraction > 0.5 || unexpected.length > 5;
|
|
17785
|
+
if (!triggered) return null;
|
|
17786
|
+
const sample = unexpected.slice(0, 5).join(", ");
|
|
17787
|
+
const more = unexpected.length > 5 ? ` (+${unexpected.length - 5} more)` : "";
|
|
17788
|
+
const pct = Math.round(fraction * 100);
|
|
17789
|
+
return `${unexpected.length}/${changed.length} changed files (${pct}%) outside FILES LIKELY TOUCHED: ${sample}${more}`;
|
|
17790
|
+
}
|
|
17641
17791
|
function getUnresolvedDeps(task, allTasks) {
|
|
17642
17792
|
if (!task.dependsOn) return [];
|
|
17643
17793
|
const deps = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
|
|
@@ -17699,6 +17849,11 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
17699
17849
|
if (task.status === "Done" || task.status === "Archived") {
|
|
17700
17850
|
throw new Error(`Task "${taskId}" (${task.title}) is already ${task.status}. Cannot execute a completed task.`);
|
|
17701
17851
|
}
|
|
17852
|
+
if (task.status === "In Review") {
|
|
17853
|
+
throw new Error(
|
|
17854
|
+
`Task "${taskId}" (${task.title}) is already In Review \u2014 it has been built and is awaiting sign-off. Run \`review_submit\` instead of re-building. If the build genuinely needs rework, use \`review_submit\` with verdict \`request-changes\` first.`
|
|
17855
|
+
);
|
|
17856
|
+
}
|
|
17702
17857
|
if (!task.buildHandoff) {
|
|
17703
17858
|
const err = new Error(`Task "${taskId}" (${task.title}) has no BUILD HANDOFF.`);
|
|
17704
17859
|
err.code = "NO_HANDOFF";
|
|
@@ -18099,6 +18254,8 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
18099
18254
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
18100
18255
|
const changed = getFilesChangedFromBase(config2.projectRoot, baseBranch);
|
|
18101
18256
|
if (changed.length > 0) report.filesChanged = changed;
|
|
18257
|
+
const drift = computeScopeDriftSignal(task.buildHandoff?.filesLikelyTouched, changed);
|
|
18258
|
+
if (drift) report.scopeDriftSignal = drift;
|
|
18102
18259
|
}
|
|
18103
18260
|
let prLines = [];
|
|
18104
18261
|
if (options.light) {
|
|
@@ -18619,8 +18776,20 @@ async function handleExecuteComplete(adapter2, config2, taskId, args, light = fa
|
|
|
18619
18776
|
const correctionsCount = args.corrections_count;
|
|
18620
18777
|
const deadEnds = args.dead_ends;
|
|
18621
18778
|
const rawBriefImplications = args.brief_implications;
|
|
18622
|
-
|
|
18623
|
-
|
|
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
|
+
);
|
|
18624
18793
|
}
|
|
18625
18794
|
const parsedEffort = parseEffortSize(effort);
|
|
18626
18795
|
const parsedEstimatedEffort = parseEffortSize(estimatedEffort);
|
|
@@ -18669,6 +18838,7 @@ function formatCompleteResult(result) {
|
|
|
18669
18838
|
`**Discovered Issues:** ${result.report.discoveredIssues}`,
|
|
18670
18839
|
`**Architecture Notes:** ${result.report.architectureNotes}`,
|
|
18671
18840
|
...result.report.deadEnds ? [`**Dead Ends:** ${result.report.deadEnds}`] : [],
|
|
18841
|
+
...result.report.scopeDriftSignal ? [`**Scope drift signal:** ${result.report.scopeDriftSignal}`] : [],
|
|
18672
18842
|
`**Scope Accuracy:** ${result.scopeAccuracy}`,
|
|
18673
18843
|
"",
|
|
18674
18844
|
"---",
|
|
@@ -21365,7 +21535,7 @@ ${lines.join("\n")}`;
|
|
|
21365
21535
|
}
|
|
21366
21536
|
let decisionLifecycleSection = "";
|
|
21367
21537
|
try {
|
|
21368
|
-
const decisions = await adapter2.getActiveDecisions();
|
|
21538
|
+
const decisions = await adapter2.getActiveDecisions({ includeRetired: true });
|
|
21369
21539
|
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
21370
21540
|
if (lifecycleSummary) {
|
|
21371
21541
|
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
@@ -21431,10 +21601,117 @@ ${lines.join("\n")}`;
|
|
|
21431
21601
|
// src/tools/orient.ts
|
|
21432
21602
|
init_git();
|
|
21433
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
|
+
|
|
21434
21710
|
// src/tools/doc-registry.ts
|
|
21435
|
-
import { readdirSync as
|
|
21436
|
-
import { join as
|
|
21711
|
+
import { readdirSync as readdirSync5, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
21712
|
+
import { join as join9, relative } from "path";
|
|
21437
21713
|
import { homedir as homedir2 } from "os";
|
|
21714
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
21438
21715
|
var docRegisterTool = {
|
|
21439
21716
|
name: "doc_register",
|
|
21440
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.",
|
|
@@ -21577,12 +21854,12 @@ ${d.summary}
|
|
|
21577
21854
|
${lines.join("\n---\n\n")}`);
|
|
21578
21855
|
}
|
|
21579
21856
|
function scanMdFiles(dir, rootDir) {
|
|
21580
|
-
if (!
|
|
21857
|
+
if (!existsSync6(dir)) return [];
|
|
21581
21858
|
const files = [];
|
|
21582
21859
|
try {
|
|
21583
|
-
const entries =
|
|
21860
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
21584
21861
|
for (const entry of entries) {
|
|
21585
|
-
const full =
|
|
21862
|
+
const full = join9(dir, entry.name);
|
|
21586
21863
|
if (entry.isDirectory()) {
|
|
21587
21864
|
files.push(...scanMdFiles(full, rootDir));
|
|
21588
21865
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -21595,7 +21872,7 @@ function scanMdFiles(dir, rootDir) {
|
|
|
21595
21872
|
}
|
|
21596
21873
|
function extractTitle(filePath) {
|
|
21597
21874
|
try {
|
|
21598
|
-
const content =
|
|
21875
|
+
const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
|
|
21599
21876
|
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
21600
21877
|
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
21601
21878
|
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
@@ -21611,17 +21888,17 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21611
21888
|
const includePlans = args.include_plans ?? false;
|
|
21612
21889
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21613
21890
|
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21614
|
-
const docsDir =
|
|
21891
|
+
const docsDir = join9(config2.projectRoot, "docs");
|
|
21615
21892
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21616
21893
|
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
21617
21894
|
let unregisteredPlans = [];
|
|
21618
21895
|
if (includePlans) {
|
|
21619
|
-
const plansDir =
|
|
21620
|
-
if (
|
|
21896
|
+
const plansDir = join9(homedir2(), ".claude", "plans");
|
|
21897
|
+
if (existsSync6(plansDir)) {
|
|
21621
21898
|
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
21622
21899
|
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
21623
21900
|
path: f,
|
|
21624
|
-
title: extractTitle(
|
|
21901
|
+
title: extractTitle(join9(plansDir, f.replace("plans/", "")))
|
|
21625
21902
|
}));
|
|
21626
21903
|
}
|
|
21627
21904
|
}
|
|
@@ -21632,7 +21909,7 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21632
21909
|
if (unregisteredDocs.length > 0) {
|
|
21633
21910
|
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
21634
21911
|
for (const f of unregisteredDocs) {
|
|
21635
|
-
const title = extractTitle(
|
|
21912
|
+
const title = extractTitle(join9(config2.projectRoot, f));
|
|
21636
21913
|
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
21637
21914
|
}
|
|
21638
21915
|
}
|
|
@@ -21645,10 +21922,106 @@ async function handleDocScan(adapter2, config2, args) {
|
|
|
21645
21922
|
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
21646
21923
|
return textResponse(lines.join("\n"));
|
|
21647
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
|
+
}
|
|
21648
22021
|
|
|
21649
22022
|
// src/services/session-guidance.ts
|
|
21650
|
-
import { existsSync as
|
|
21651
|
-
import { join as
|
|
22023
|
+
import { existsSync as existsSync7 } from "fs";
|
|
22024
|
+
import { join as join10 } from "path";
|
|
21652
22025
|
var state = {
|
|
21653
22026
|
toolCallCount: 0,
|
|
21654
22027
|
lastOrientAt: null,
|
|
@@ -21669,8 +22042,8 @@ async function buildSessionGuidance(adapter2, projectRoot) {
|
|
|
21669
22042
|
const signals = [];
|
|
21670
22043
|
try {
|
|
21671
22044
|
if (adapter2.searchDocs) {
|
|
21672
|
-
const researchDir =
|
|
21673
|
-
if (
|
|
22045
|
+
const researchDir = join10(projectRoot, "docs", "research");
|
|
22046
|
+
if (existsSync7(researchDir)) {
|
|
21674
22047
|
const files = scanMdFiles(researchDir, projectRoot);
|
|
21675
22048
|
if (files.length > 0) {
|
|
21676
22049
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -21707,8 +22080,8 @@ async function buildSessionGuidance(adapter2, projectRoot) {
|
|
|
21707
22080
|
|
|
21708
22081
|
// src/tools/orient.ts
|
|
21709
22082
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
21710
|
-
import { readFileSync as
|
|
21711
|
-
import { join as
|
|
22083
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync8 } from "fs";
|
|
22084
|
+
import { join as join11 } from "path";
|
|
21712
22085
|
var GIT_DEPENDENT_ENVS = /* @__PURE__ */ new Set(["cowork", "api"]);
|
|
21713
22086
|
var VALID_ENVS = /* @__PURE__ */ new Set(["claude-code", "cowork", "api", "unknown"]);
|
|
21714
22087
|
function normaliseEnvironment(raw) {
|
|
@@ -21864,10 +22237,10 @@ async function getHierarchyPosition(adapter2) {
|
|
|
21864
22237
|
adapter2.queryBoard()
|
|
21865
22238
|
]);
|
|
21866
22239
|
if (horizons.length === 0) return void 0;
|
|
21867
|
-
const
|
|
21868
|
-
const activeHorizon = horizons.find(
|
|
22240
|
+
const isInProgress = (s) => s.status === "In Progress";
|
|
22241
|
+
const activeHorizon = horizons.find(isInProgress) || horizons[0];
|
|
21869
22242
|
const horizonStages = stages.filter((s) => s.horizonId === activeHorizon.id).sort((a, b2) => (a.sortOrder ?? 0) - (b2.sortOrder ?? 0));
|
|
21870
|
-
const activeStage = horizonStages.find(
|
|
22243
|
+
const activeStage = horizonStages.find(isInProgress) || horizonStages.find((s) => s.status !== "Done") || horizonStages[0];
|
|
21871
22244
|
if (!activeStage) return void 0;
|
|
21872
22245
|
const stagePhases = phases.filter((p) => p.stageId === activeStage.id);
|
|
21873
22246
|
const activePhases = stagePhases.filter((p) => p.status === "In Progress");
|
|
@@ -21908,8 +22281,8 @@ function getLatestGitTag(projectRoot) {
|
|
|
21908
22281
|
}
|
|
21909
22282
|
function checkNpmVersionDrift() {
|
|
21910
22283
|
try {
|
|
21911
|
-
const pkgPath =
|
|
21912
|
-
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"));
|
|
21913
22286
|
const localVersion = pkg.version;
|
|
21914
22287
|
const packageName = pkg.name;
|
|
21915
22288
|
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
@@ -21925,6 +22298,44 @@ function checkNpmVersionDrift() {
|
|
|
21925
22298
|
return null;
|
|
21926
22299
|
}
|
|
21927
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
|
+
}
|
|
21928
22339
|
async function handleOrient(adapter2, config2, args = {}) {
|
|
21929
22340
|
const environment = normaliseEnvironment(args.environment);
|
|
21930
22341
|
try {
|
|
@@ -22035,7 +22446,7 @@ ${versionDrift}` : "";
|
|
|
22035
22446
|
let unregisteredDocsNote = "";
|
|
22036
22447
|
try {
|
|
22037
22448
|
if (adapter2.searchDocs) {
|
|
22038
|
-
const docsDir =
|
|
22449
|
+
const docsDir = join11(config2.projectRoot, "docs");
|
|
22039
22450
|
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
22040
22451
|
if (docsFiles.length > 0) {
|
|
22041
22452
|
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
@@ -22086,7 +22497,7 @@ ${versionDrift}` : "";
|
|
|
22086
22497
|
lines.push(` \u2192 \u2026and ${pendingActions.length - 2} more`);
|
|
22087
22498
|
}
|
|
22088
22499
|
}
|
|
22089
|
-
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._");
|
|
22090
22501
|
researchSignalsNote = lines.join("\n");
|
|
22091
22502
|
}
|
|
22092
22503
|
}
|
|
@@ -22130,36 +22541,38 @@ ${versionDrift}` : "";
|
|
|
22130
22541
|
try {
|
|
22131
22542
|
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 30 });
|
|
22132
22543
|
if (learnings) {
|
|
22133
|
-
const
|
|
22134
|
-
const
|
|
22135
|
-
|
|
22136
|
-
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
|
|
22140
|
-
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
if (allAlerts.length > 0) {
|
|
22144
|
-
const header = allAlerts.length > ALERT_CAP ? `${allAlerts.length} P0/P1 discovered issues awaiting action (showing ${ALERT_CAP} most recent).` : `${allAlerts.length} P0/P1 discovered issue${allAlerts.length !== 1 ? "s" : ""} awaiting action.`;
|
|
22145
|
-
const lines = ["\n\n## \u{1F6A8} Alerts", header];
|
|
22146
|
-
for (const issue of alerts) {
|
|
22147
|
-
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
22148
|
-
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
22544
|
+
const candidateLearnings = learnings.filter((l) => !l.actionTaken);
|
|
22545
|
+
const referencedTaskIds = Array.from(new Set(candidateLearnings.map((l) => l.taskId).filter(Boolean)));
|
|
22546
|
+
let closedTaskIds = /* @__PURE__ */ new Set();
|
|
22547
|
+
if (referencedTaskIds.length > 0) {
|
|
22548
|
+
try {
|
|
22549
|
+
const tasks = await adapter2.getTasks(referencedTaskIds);
|
|
22550
|
+
closedTaskIds = new Set(
|
|
22551
|
+
tasks.filter((t) => t.status === "Done" || t.status === "Cancelled").map((t) => t.id)
|
|
22552
|
+
);
|
|
22553
|
+
} catch {
|
|
22149
22554
|
}
|
|
22150
|
-
lines.push("_Escalate: run `idea` with P1 priority, or `board_edit` if already handled._");
|
|
22151
|
-
alertsNote = lines.join("\n");
|
|
22152
22555
|
}
|
|
22153
|
-
|
|
22154
|
-
|
|
22155
|
-
|
|
22156
|
-
|
|
22157
|
-
|
|
22158
|
-
|
|
22159
|
-
|
|
22160
|
-
|
|
22161
|
-
|
|
22162
|
-
|
|
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 {
|
|
22163
22576
|
}
|
|
22164
22577
|
}
|
|
22165
22578
|
} catch {
|
|
@@ -22175,16 +22588,16 @@ ${versionDrift}` : "";
|
|
|
22175
22588
|
markOrient();
|
|
22176
22589
|
} catch {
|
|
22177
22590
|
}
|
|
22178
|
-
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);
|
|
22179
22592
|
} catch (err) {
|
|
22180
22593
|
const message = err instanceof Error ? err.message : String(err);
|
|
22181
22594
|
return errorResponse(`Orient failed: ${message}`);
|
|
22182
22595
|
}
|
|
22183
22596
|
}
|
|
22184
22597
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
22185
|
-
const claudeMdPath =
|
|
22186
|
-
if (!
|
|
22187
|
-
const content =
|
|
22598
|
+
const claudeMdPath = join11(projectRoot, "CLAUDE.md");
|
|
22599
|
+
if (!existsSync8(claudeMdPath)) return "";
|
|
22600
|
+
const content = readFileSync4(claudeMdPath, "utf-8");
|
|
22188
22601
|
const additions = [];
|
|
22189
22602
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
22190
22603
|
additions.push(CLAUDE_MD_TIER_1);
|
|
@@ -22966,12 +23379,12 @@ ${result.userMessage}
|
|
|
22966
23379
|
}
|
|
22967
23380
|
|
|
22968
23381
|
// src/lib/install-id.ts
|
|
22969
|
-
import { randomUUID as
|
|
22970
|
-
import { mkdirSync, readFileSync as
|
|
23382
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
23383
|
+
import { mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync } from "fs";
|
|
22971
23384
|
import { homedir as homedir3 } from "os";
|
|
22972
|
-
import { join as
|
|
22973
|
-
var PAPI_HOME_DIR =
|
|
22974
|
-
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");
|
|
22975
23388
|
var cachedInstallId = null;
|
|
22976
23389
|
function isValidUuid(s) {
|
|
22977
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);
|
|
@@ -22979,7 +23392,7 @@ function isValidUuid(s) {
|
|
|
22979
23392
|
function getInstallId() {
|
|
22980
23393
|
if (cachedInstallId) return cachedInstallId;
|
|
22981
23394
|
try {
|
|
22982
|
-
const raw =
|
|
23395
|
+
const raw = readFileSync5(INSTALL_ID_FILE, "utf-8");
|
|
22983
23396
|
const parsed = JSON.parse(raw);
|
|
22984
23397
|
if (isValidUuid(parsed.install_id)) {
|
|
22985
23398
|
cachedInstallId = parsed.install_id;
|
|
@@ -22989,7 +23402,7 @@ function getInstallId() {
|
|
|
22989
23402
|
}
|
|
22990
23403
|
try {
|
|
22991
23404
|
mkdirSync(PAPI_HOME_DIR, { recursive: true, mode: 448 });
|
|
22992
|
-
const id =
|
|
23405
|
+
const id = randomUUID16();
|
|
22993
23406
|
const contents = {
|
|
22994
23407
|
install_id: id,
|
|
22995
23408
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -23109,7 +23522,7 @@ function createServer(adapter2, config2) {
|
|
|
23109
23522
|
const __pkgDir = dirname(__pkgFilename);
|
|
23110
23523
|
let serverVersion = "unknown";
|
|
23111
23524
|
try {
|
|
23112
|
-
const pkg = JSON.parse(
|
|
23525
|
+
const pkg = JSON.parse(readFileSync6(join13(__pkgDir, "..", "package.json"), "utf-8"));
|
|
23113
23526
|
serverVersion = pkg.version ?? "unknown";
|
|
23114
23527
|
} catch {
|
|
23115
23528
|
}
|
|
@@ -23124,7 +23537,7 @@ function createServer(adapter2, config2) {
|
|
|
23124
23537
|
}
|
|
23125
23538
|
const __filename = fileURLToPath(import.meta.url);
|
|
23126
23539
|
const __dirname2 = dirname(__filename);
|
|
23127
|
-
const skillsDir =
|
|
23540
|
+
const skillsDir = join13(__dirname2, "..", "skills");
|
|
23128
23541
|
function parseSkillFrontmatter(content) {
|
|
23129
23542
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
23130
23543
|
if (!match) return null;
|
|
@@ -23142,7 +23555,7 @@ function createServer(adapter2, config2) {
|
|
|
23142
23555
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
23143
23556
|
const prompts = [];
|
|
23144
23557
|
for (const file of mdFiles) {
|
|
23145
|
-
const content = await readFile5(
|
|
23558
|
+
const content = await readFile5(join13(skillsDir, file), "utf-8");
|
|
23146
23559
|
const meta = parseSkillFrontmatter(content);
|
|
23147
23560
|
if (meta) {
|
|
23148
23561
|
prompts.push({ name: meta.name, description: meta.description });
|
|
@@ -23158,7 +23571,7 @@ function createServer(adapter2, config2) {
|
|
|
23158
23571
|
try {
|
|
23159
23572
|
const files = await readdir2(skillsDir);
|
|
23160
23573
|
for (const file of files.filter((f) => f.endsWith(".md"))) {
|
|
23161
|
-
const content = await readFile5(
|
|
23574
|
+
const content = await readFile5(join13(skillsDir, file), "utf-8");
|
|
23162
23575
|
const meta = parseSkillFrontmatter(content);
|
|
23163
23576
|
if (meta?.name === name) {
|
|
23164
23577
|
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, "");
|
|
@@ -23201,6 +23614,7 @@ function createServer(adapter2, config2) {
|
|
|
23201
23614
|
docRegisterTool,
|
|
23202
23615
|
docSearchTool,
|
|
23203
23616
|
docScanTool,
|
|
23617
|
+
docActionPromoteTool,
|
|
23204
23618
|
getSiblingAdsTool,
|
|
23205
23619
|
handoffGenerateTool
|
|
23206
23620
|
]
|
|
@@ -23316,6 +23730,9 @@ function createServer(adapter2, config2) {
|
|
|
23316
23730
|
case "doc_scan":
|
|
23317
23731
|
result = await handleDocScan(adapter2, config2, safeArgs);
|
|
23318
23732
|
break;
|
|
23733
|
+
case "doc_action_promote":
|
|
23734
|
+
result = await handleDocActionPromote(adapter2, safeArgs);
|
|
23735
|
+
break;
|
|
23319
23736
|
case "get_sibling_ads":
|
|
23320
23737
|
result = await handleGetSiblingAds(adapter2, safeArgs);
|
|
23321
23738
|
break;
|
|
@@ -23371,7 +23788,7 @@ function createServer(adapter2, config2) {
|
|
|
23371
23788
|
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
23372
23789
|
var pkgVersion = "unknown";
|
|
23373
23790
|
try {
|
|
23374
|
-
const pkg = JSON.parse(
|
|
23791
|
+
const pkg = JSON.parse(readFileSync7(join14(__dirname, "..", "package.json"), "utf-8"));
|
|
23375
23792
|
pkgVersion = pkg.version;
|
|
23376
23793
|
} catch {
|
|
23377
23794
|
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
// src/prompts.ts
|
|
2
|
+
var AD_REJECTION_RULES = `**AD Minting Guard \u2014 REJECT observations dressed as decisions.**
|
|
3
|
+
|
|
4
|
+
An Active Decision expresses a *stance* the project is taking \u2014 a choice between alternatives that constrains future work. Reject any candidate AD whose body asserts:
|
|
5
|
+
(a) the existence, identity, or status of a person, user, or external entity (e.g. "User X is building Y", "Customer Z is active");
|
|
6
|
+
(b) a metric or measurement (e.g. "Signups grew 3x last cycle", "Latency dropped to 200ms");
|
|
7
|
+
(c) a fact about the current state of the world that could be confirmed or denied by a query rather than challenged by argument (e.g. "External user feedback is now flowing", "The /admin route exists").
|
|
8
|
+
|
|
9
|
+
If a candidate AD body could be invalidated by running a SQL query, refreshing a dashboard, or checking a log \u2014 it is an observation, not a decision. Capture it as a build report finding, a dogfood observation, a cycle log note, or a registered doc instead. Do NOT mint it as an AD.
|
|
10
|
+
|
|
11
|
+
**Positive example (valid AD):** "Pricing tier strategy: free engine + paid intelligence. Decision: keep cycles free, charge for strategy reviews and analytics. Why: telemetry shows engagement clusters around intelligence surfaces, not engine surfaces." \u2014 this is a stance with alternatives.
|
|
12
|
+
|
|
13
|
+
**Negative example (reject):** "External user feedback is now flowing. Stonebridge Systems is actively building." \u2014 this is a fact about the current state of the world. Capture as dogfood/signal observation; do not mint.
|
|
14
|
+
|
|
15
|
+
This rule applies to: new ADs proposed during planning (Step 9), strategy review AD updates (section 5), and strategy_change AD updates. If you find an existing AD that violates this rule during housekeeping, propose deleting it (action: "delete") with a one-line rationale.`;
|
|
2
16
|
var PLAN_SYSTEM = `You are the PAPI Cycle Planner \u2014 an autonomous planning engine for software projects.
|
|
3
17
|
You receive project context and produce a planning cycle output with a BUILD HANDOFF.
|
|
4
18
|
|
|
@@ -179,6 +193,7 @@ Standard planning cycle with full board review.
|
|
|
179
193
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
180
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.
|
|
181
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.
|
|
182
197
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
183
198
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
184
199
|
|
|
@@ -224,6 +239,9 @@ Standard planning cycle with full board review.
|
|
|
224
239
|
|
|
225
240
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
226
241
|
**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.
|
|
242
|
+
|
|
243
|
+
${AD_REJECTION_RULES}
|
|
244
|
+
|
|
227
245
|
**\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.
|
|
228
246
|
|
|
229
247
|
### Operational Quality Rules
|
|
@@ -414,6 +432,7 @@ Standard planning cycle with full board review.
|
|
|
414
432
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
415
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.
|
|
416
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.
|
|
417
436
|
If a task is clearly obsolete, duplicated, or rejected, set its status to "Cancelled" with a \`closureReason\` explaining why.
|
|
418
437
|
**\u2192 PERSIST:** For each task you set reviewed: true, corrected fields on, or marked "Cancelled", include it in \`boardCorrections\` in Part 2.
|
|
419
438
|
|
|
@@ -461,6 +480,9 @@ Standard planning cycle with full board review.
|
|
|
461
480
|
|
|
462
481
|
9. **Active Decisions** \u2014 If any AD needs updating: Type A (confidence change), Type B (modification), or Type C (reversal/supersede).
|
|
463
482
|
**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.
|
|
483
|
+
|
|
484
|
+
${AD_REJECTION_RULES}
|
|
485
|
+
|
|
464
486
|
**\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.
|
|
465
487
|
|
|
466
488
|
### Operational Quality Rules
|
|
@@ -625,6 +647,16 @@ function buildPlanUserMessage(ctx) {
|
|
|
625
647
|
if (ctx.registeredDocs) {
|
|
626
648
|
parts.push("### Relevant Research Docs", "", ctx.registeredDocs, "");
|
|
627
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
|
+
}
|
|
628
660
|
if (ctx.carryForwardStaleness) {
|
|
629
661
|
parts.push("### Carry-Forward Staleness", "", ctx.carryForwardStaleness, "");
|
|
630
662
|
}
|
|
@@ -833,8 +865,17 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
833
865
|
- Note any hierarchy/phase issues worth correcting (1-2 bullets max)
|
|
834
866
|
- Delete ADs that are legacy, process-level, or redundant without discussion
|
|
835
867
|
|
|
868
|
+
${AD_REJECTION_RULES}
|
|
869
|
+
|
|
836
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.
|
|
837
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
|
+
|
|
838
879
|
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
839
880
|
|
|
840
881
|
6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
|
|
@@ -1142,6 +1183,8 @@ The JSON must be valid. Only include ADs that need changes \u2014 omit unchanged
|
|
|
1142
1183
|
For new ADs, use the next available AD number.
|
|
1143
1184
|
The body field must be the COMPLETE replacement text for the AD block (including the ### heading line).
|
|
1144
1185
|
|
|
1186
|
+
${AD_REJECTION_RULES}
|
|
1187
|
+
|
|
1145
1188
|
## PHASE UPDATES
|
|
1146
1189
|
|
|
1147
1190
|
If the strategic change affects the project's phase structure, include a phaseUpdates array.
|
|
@@ -1438,6 +1481,7 @@ ${inputs.codebaseContext}
|
|
|
1438
1481
|
Return a JSON array of 3-10 tasks based on gaps, improvements, and next steps visible from the codebase analysis above.`;
|
|
1439
1482
|
}
|
|
1440
1483
|
export {
|
|
1484
|
+
AD_REJECTION_RULES,
|
|
1441
1485
|
AD_SEED_SYSTEM,
|
|
1442
1486
|
CONVENTIONS_SYSTEM,
|
|
1443
1487
|
HANDOFF_REGEN_SYSTEM,
|
package/package.json
CHANGED