@agent-plan/pi-adapter 0.2.24 → 0.2.26
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.d.ts.map +1 -1
- package/dist/index.js +757 -118
- package/package.json +3 -3
- package/web-ui-dist/assets/index-C9NZu-jP.css +1 -0
- package/web-ui-dist/assets/index-DOhGgMMb.js +328 -0
- package/web-ui-dist/index.html +2 -2
- package/web-ui-dist/assets/index-COpkshJD.css +0 -1
- package/web-ui-dist/assets/index-IyxH71Dr.js +0 -298
package/dist/index.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { paginatedSelect, paginatedNotify } from "./ui/paginate.js";
|
|
13
|
-
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureReadForSessionId, markPhaseReadForSessionId, markTaskReadForSessionId, contextReadEligibilityForSession, hasValidSessionAttestation, hasReadRequirementsForSession, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded } from "@agent-plan/core";
|
|
14
|
-
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
13
|
+
import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureReadForSessionId, markPhaseReadForSessionId, markTaskReadForSessionId, contextReadEligibilityForSession, hasValidSessionAttestation, hasReadRequirementsForSession, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, MAX_HANDOFF_CONTENT_CHARS, HandoffContractError } from "@agent-plan/core";
|
|
14
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, formatIdeaRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
18
18
|
import { writeFile, rm } from "node:fs/promises";
|
|
19
|
-
import { randomUUID } from "node:crypto";
|
|
20
19
|
import { createServer } from "node:net";
|
|
21
20
|
import { fileURLToPath } from "node:url";
|
|
22
21
|
import { serve } from "@agent-plan/server/serve";
|
|
@@ -30,7 +29,7 @@ let capturedPi = null;
|
|
|
30
29
|
let store = null;
|
|
31
30
|
let server = null;
|
|
32
31
|
let lastKnownWebPort = null;
|
|
33
|
-
let plannerSessionId =
|
|
32
|
+
let plannerSessionId = "pi:uninitialized";
|
|
34
33
|
// True while the agent is mutating .planner/ files. The web server returns 503-busy
|
|
35
34
|
// during this window so the UI doesn't render inconsistent data.
|
|
36
35
|
let planBusy = false;
|
|
@@ -55,6 +54,97 @@ function taskStartFailure(outcome, details = {}) {
|
|
|
55
54
|
details: { ...outcome, ...details },
|
|
56
55
|
};
|
|
57
56
|
}
|
|
57
|
+
function derivedStatusReadOnlyResult(entity, ref, attemptedStatus, effectiveStatus) {
|
|
58
|
+
const childKind = entity === "feature" ? "phases" : "tasks";
|
|
59
|
+
const retryAction = entity === "feature"
|
|
60
|
+
? `Retry feature_update ${ref} without the status field if you need to update metadata.`
|
|
61
|
+
: `Retry phase_update ${ref} without the status field if you need to update metadata.`;
|
|
62
|
+
const nextActions = [
|
|
63
|
+
`${entity[0].toUpperCase()}${entity.slice(1)} status is derived from child ${childKind}; update the child ${childKind} instead of setting ${entity}.status.`,
|
|
64
|
+
retryAction,
|
|
65
|
+
];
|
|
66
|
+
return {
|
|
67
|
+
isError: true,
|
|
68
|
+
content: [{ type: "text", text: [
|
|
69
|
+
`❌ ${entity.toUpperCase()} UPDATE FAILED [DERIVED_STATUS_READ_ONLY]`,
|
|
70
|
+
`${entity[0].toUpperCase()}${entity.slice(1)} status is derived from child ${childKind} and cannot be set directly.`,
|
|
71
|
+
`Attempted status: ${attemptedStatus}`,
|
|
72
|
+
`Effective derived status: ${effectiveStatus}`,
|
|
73
|
+
"No planner data was changed.",
|
|
74
|
+
"Next required actions:",
|
|
75
|
+
...nextActions.map((action, index) => `${index + 1}. ${action}`),
|
|
76
|
+
].join("\n") }],
|
|
77
|
+
details: {
|
|
78
|
+
updated: false,
|
|
79
|
+
errorCode: "DERIVED_STATUS_READ_ONLY",
|
|
80
|
+
entity,
|
|
81
|
+
ref,
|
|
82
|
+
attemptedStatus,
|
|
83
|
+
effectiveStatus,
|
|
84
|
+
nextActions,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function mutationNoFieldsFailure(input) {
|
|
89
|
+
const outcome = noMutableFieldsReceived(input);
|
|
90
|
+
return {
|
|
91
|
+
isError: true,
|
|
92
|
+
content: [{ type: "text", text: outcome.message }],
|
|
93
|
+
details: outcome,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function legacyContextMigrationSummary(preview) {
|
|
97
|
+
return [
|
|
98
|
+
`Legacy context present: ${preview.hasLegacyContext ? "yes" : "no"}`,
|
|
99
|
+
`Guideline additions: ${preview.guidelineAdditions.length} (${preview.skippedGuidelineDuplicates} duplicate(s) skipped)`,
|
|
100
|
+
`Accepted decision additions: ${preview.acceptedDecisionAdditions.length} (${preview.skippedDecisionDuplicates} duplicate(s) skipped)`,
|
|
101
|
+
`Legacy counts: globalRules=${preview.legacyCounts.globalRules}, workflowRules=${preview.legacyCounts.workflowRules}, decisions=${preview.legacyCounts.decisions}`,
|
|
102
|
+
`Fields cleared on apply: ${preview.fieldsClearedOnApply.join(", ") || "none"}`,
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
function handoffContractFailure(error) {
|
|
106
|
+
return {
|
|
107
|
+
isError: true,
|
|
108
|
+
content: [{ type: "text", text: `❌ Handoff refresh denied [${error.code}]: ${error.message}` }],
|
|
109
|
+
details: { errorCode: error.code, ...error.details },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function boundedHandoffForTransport(content) {
|
|
113
|
+
if (content.length <= MAX_HANDOFF_CONTENT_CHARS)
|
|
114
|
+
return { content, fullLength: content.length, truncated: false };
|
|
115
|
+
const suffix = "\n\n[Legacy handoff truncated for transport safety. Move extended detail to a linked file under .planner/docs/ and refresh the handoff.]";
|
|
116
|
+
return {
|
|
117
|
+
content: `${content.slice(0, MAX_HANDOFF_CONTENT_CHARS - suffix.length)}${suffix}`,
|
|
118
|
+
fullLength: content.length,
|
|
119
|
+
truncated: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function boundedPage(items, page, pageSize) {
|
|
123
|
+
const size = Math.min(25, Math.max(1, Math.trunc(pageSize)));
|
|
124
|
+
const totalPages = Math.max(1, Math.ceil(items.length / size));
|
|
125
|
+
const currentPage = Math.min(totalPages, Math.max(1, Math.trunc(page)));
|
|
126
|
+
return {
|
|
127
|
+
items: items.slice((currentPage - 1) * size, currentPage * size),
|
|
128
|
+
page: currentPage,
|
|
129
|
+
pageSize: size,
|
|
130
|
+
total: items.length,
|
|
131
|
+
totalPages,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function piContextReadActions(eligibility, refs, projectGuidelinesReadRequired, requirementReadRequired, retryAction) {
|
|
135
|
+
const actions = projectGuidelinesReadRequired ? ["project_guidelines_show"] : [];
|
|
136
|
+
actions.push(...(eligibility.requiredReads ?? []).flatMap((read) => {
|
|
137
|
+
if (read.kind === "task")
|
|
138
|
+
return [`task_get ${refs.task} with full=true`];
|
|
139
|
+
if (read.kind === "phase")
|
|
140
|
+
return [`phase_get ${refs.phase} with full=true`];
|
|
141
|
+
return refs.feature ? [`feature_get ${refs.feature} with full=true`] : [];
|
|
142
|
+
}));
|
|
143
|
+
if (requirementReadRequired)
|
|
144
|
+
actions.push("requirement_list");
|
|
145
|
+
actions.push(retryAction);
|
|
146
|
+
return actions;
|
|
147
|
+
}
|
|
58
148
|
// Helper to notify a running server in another process via HTTP.
|
|
59
149
|
async function notifyExternalServer() {
|
|
60
150
|
try {
|
|
@@ -129,6 +219,8 @@ const PLANNER_COMMAND_COMPLETIONS = [
|
|
|
129
219
|
{ value: "cleanup-orphans", label: "cleanup-orphans", description: "List and remove orphan phase files" },
|
|
130
220
|
{ value: "project discuss", label: "project discuss", description: "Run project discovery" },
|
|
131
221
|
{ value: "project language", label: "project language", description: "Set persistent language preferences" },
|
|
222
|
+
{ value: "project guidelines", label: "project guidelines", description: "Edit the canonical Project Guidelines section" },
|
|
223
|
+
{ value: "project migrate-context", label: "project migrate-context", description: "Preview and explicitly migrate legacy project rules and decisions" },
|
|
132
224
|
{ value: "feature list", label: "feature list", description: "List features" },
|
|
133
225
|
{ value: "feature add", label: "feature add", description: "Create a feature" },
|
|
134
226
|
{ value: "feature show", label: "feature show", description: "Show a feature" },
|
|
@@ -802,8 +894,8 @@ export default function planPiExtension(pi) {
|
|
|
802
894
|
capturedPi = pi;
|
|
803
895
|
// ── Restore on session start/reload ─────────────────────────────────
|
|
804
896
|
pi.on("session_start", async (_event, ctx) => {
|
|
805
|
-
plannerSessionId =
|
|
806
|
-
invalidateReads();
|
|
897
|
+
plannerSessionId = `pi:${ctx.sessionManager.getSessionId()}`;
|
|
898
|
+
invalidateReads(plannerSessionId);
|
|
807
899
|
startReadSession(plannerSessionId);
|
|
808
900
|
try {
|
|
809
901
|
ctx.ui.addAutocompleteProvider((current) => ({
|
|
@@ -1096,6 +1188,13 @@ export default function planPiExtension(pi) {
|
|
|
1096
1188
|
"cleanup-orphans",
|
|
1097
1189
|
"project discuss",
|
|
1098
1190
|
"project language",
|
|
1191
|
+
"project guidelines",
|
|
1192
|
+
"idea list",
|
|
1193
|
+
"idea add",
|
|
1194
|
+
"idea show",
|
|
1195
|
+
"idea update",
|
|
1196
|
+
"idea delete",
|
|
1197
|
+
"idea promote",
|
|
1099
1198
|
"feature list",
|
|
1100
1199
|
"feature add",
|
|
1101
1200
|
"feature show",
|
|
@@ -1127,7 +1226,7 @@ export default function planPiExtension(pi) {
|
|
|
1127
1226
|
"load",
|
|
1128
1227
|
"stop",
|
|
1129
1228
|
];
|
|
1130
|
-
const SUB_HELP = "Available: init, show, version, repair, cleanup-orphans, project, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
|
|
1229
|
+
const SUB_HELP = "Available: init, show, version, repair, cleanup-orphans, project, idea, feature, phase, task, discuss, handoff, web, export, export-full, bypass, clear-bypass, load, stop\n" +
|
|
1131
1230
|
"Try: /planner <TAB> | /planner feature list | /planner task start | /planner cleanup-orphans\n" +
|
|
1132
1231
|
"Handoff actions: /planner handoff list | show P00x | write P00x | clear P00x | prepare (prepare proposes a target and asks for confirmation)";
|
|
1133
1232
|
if (!a) {
|
|
@@ -1429,7 +1528,7 @@ export default function planPiExtension(pi) {
|
|
|
1429
1528
|
// ═══════════════════════════════════════════════════════════════
|
|
1430
1529
|
if (a === "project") {
|
|
1431
1530
|
if (!b) {
|
|
1432
|
-
ctx.ui.notify("project actions: discuss | language", "info");
|
|
1531
|
+
ctx.ui.notify("project actions: discuss | language | guidelines | migrate-context", "info");
|
|
1433
1532
|
return;
|
|
1434
1533
|
}
|
|
1435
1534
|
if (b === "discuss") {
|
|
@@ -1491,7 +1590,115 @@ export default function planPiExtension(pi) {
|
|
|
1491
1590
|
ctx.ui.notify(`Saved language preferences: content=${project.contentLanguage}, chat=${project.chatLanguage}`, "info");
|
|
1492
1591
|
return;
|
|
1493
1592
|
}
|
|
1494
|
-
|
|
1593
|
+
if (b === "guidelines") {
|
|
1594
|
+
const project = await st.loadProject();
|
|
1595
|
+
const nextContent = await ctx.ui.editor("Project Guidelines / Coding Standard (Markdown or plain text). Agents must read this before task work.", project.projectGuidelines.content || "");
|
|
1596
|
+
project.projectGuidelines = {
|
|
1597
|
+
content: nextContent?.trim() || "",
|
|
1598
|
+
updatedAt: project.projectGuidelines.updatedAt,
|
|
1599
|
+
sessionInfo: project.projectGuidelines.sessionInfo,
|
|
1600
|
+
};
|
|
1601
|
+
await st.saveProject(project);
|
|
1602
|
+
await st.writeGenerated();
|
|
1603
|
+
ctx.ui.notify(project.projectGuidelines.content ? "Project Guidelines updated." : "Project Guidelines cleared.", "info");
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
if (b === "migrate-context") {
|
|
1607
|
+
const preview = await st.previewLegacyProjectContextMigration();
|
|
1608
|
+
const summary = legacyContextMigrationSummary(preview);
|
|
1609
|
+
if (!preview.hasLegacyContext) {
|
|
1610
|
+
ctx.ui.notify(`Project context migration preview:\n${summary}\nNo migration is needed.`, "info");
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const choice = await ctx.ui.select("Apply the previewed migration? This clears only migrated legacy fields.", ["apply", "cancel"]);
|
|
1614
|
+
if (choice !== "apply") {
|
|
1615
|
+
ctx.ui.notify(`Project context migration preview (no changes applied):\n${summary}`, "info");
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const result = await st.migrateLegacyProjectContext();
|
|
1619
|
+
await st.writeGenerated();
|
|
1620
|
+
ctx.ui.notify(`${result.applied ? "Legacy project context migration applied and verified." : "No legacy project context required migration."}\n${legacyContextMigrationSummary(result.preview)}`, "info");
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
ctx.ui.notify(`Unknown project action "${b}". Try: discuss, language, guidelines, migrate-context`, "warning");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1627
|
+
// idea <sub>
|
|
1628
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1629
|
+
if (a === "idea") {
|
|
1630
|
+
if (!b) {
|
|
1631
|
+
ctx.ui.notify("idea actions: list | add [title] | show [ref] | update [ref] | delete [ref] | promote [ref]", "info");
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const ideas = (await st.loadIdeas()).ideas;
|
|
1635
|
+
if (b === "list") {
|
|
1636
|
+
await paginatedNotify(ctx, {
|
|
1637
|
+
title: "ideas",
|
|
1638
|
+
lines: ideas.map((idea) => `${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}${idea.promotion ? ` → ${idea.promotion.targetRef}` : ""}`),
|
|
1639
|
+
pageSize: 10,
|
|
1640
|
+
});
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (b === "add") {
|
|
1644
|
+
const title = subArgs.trim() || await ctx.ui.input("Idea title");
|
|
1645
|
+
if (!title?.trim()) {
|
|
1646
|
+
ctx.ui.notify("Aborted", "warning");
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const description = await ctx.ui.editor("Idea description", "");
|
|
1650
|
+
const idea = await st.createIdea({ title: title.trim(), ...(description?.trim() ? { description: description.trim() } : {}) });
|
|
1651
|
+
await st.writeGenerated();
|
|
1652
|
+
ctx.ui.notify(`Idea created: ${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}`, "info");
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const ref = subArgs.trim() || await ctx.ui.input("Idea ref (I00x, short ID, or title)");
|
|
1656
|
+
const idea = ref ? findIdeaByRef(ideas, ref) : undefined;
|
|
1657
|
+
if (!idea) {
|
|
1658
|
+
ctx.ui.notify(`Idea not found: ${ref || "(empty)"}`, "error");
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (b === "show") {
|
|
1662
|
+
ctx.ui.notify(`${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}\n\n${idea.description || "(no description)"}${idea.promotion ? `\n\nPromoted to: ${idea.promotion.targetRef}` : ""}`, "info");
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (b === "update") {
|
|
1666
|
+
const title = await ctx.ui.input(`Title [${idea.title}]`);
|
|
1667
|
+
const description = await ctx.ui.editor("Description", idea.description);
|
|
1668
|
+
const updated = await st.updateIdea(idea.id, { ...(title?.trim() ? { title: title.trim() } : {}), ...(description !== undefined ? { description: description.trim() } : {}) });
|
|
1669
|
+
await st.writeGenerated();
|
|
1670
|
+
ctx.ui.notify(`Idea updated: ${formatIdeaRef(updated.number)} — ${updated.title}`, "info");
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
if (b === "delete") {
|
|
1674
|
+
const answer = await ctx.ui.select(`Delete ${formatIdeaRef(idea.number)} — ${idea.title}?`, ["cancel", "delete"]);
|
|
1675
|
+
if (answer !== "delete") {
|
|
1676
|
+
ctx.ui.notify("Cancelled — no changes saved.", "info");
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
await st.deleteIdea(idea.id);
|
|
1680
|
+
await st.writeGenerated();
|
|
1681
|
+
ctx.ui.notify(`Idea deleted: ${formatIdeaRef(idea.number)}.`, "info");
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (b === "promote") {
|
|
1685
|
+
if (idea.promotion) {
|
|
1686
|
+
ctx.ui.notify(`Already promoted to ${idea.promotion.targetRef}.`, "warning");
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
const targetType = await ctx.ui.select("Promotion target", ["feature", "phase", "task"]);
|
|
1690
|
+
if (!targetType)
|
|
1691
|
+
return;
|
|
1692
|
+
const skill = await st.ideaDiscussionSkill();
|
|
1693
|
+
if (capturedPi) {
|
|
1694
|
+
await capturedPi.sendUserMessage(`Discuss promotion of ${formatIdeaRef(idea.number)} — ${idea.title} to a ${targetType}.\n\n${skill}\n\nDo not create or persist a target until the discussion is complete and I explicitly confirm the exact destination. For a phase, recommend and confirm its feature. For a task, recommend and confirm its feature and phase. After creating the confirmed target with the normal planner tool, record the promotion with idea_promotion_finalize.`);
|
|
1695
|
+
}
|
|
1696
|
+
else {
|
|
1697
|
+
ctx.ui.notify("Promotion discussion instructions loaded. Use the idea_promotion_begin tool in an agent session.", "info");
|
|
1698
|
+
}
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
ctx.ui.notify(`Unknown idea action "${b}". Try: list, add, show, update, delete, promote`, "warning");
|
|
1495
1702
|
return;
|
|
1496
1703
|
}
|
|
1497
1704
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1773,7 +1980,7 @@ export default function planPiExtension(pi) {
|
|
|
1773
1980
|
goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
|
|
1774
1981
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
1775
1982
|
createdAt: nowISO(), updatedAt: nowISO(),
|
|
1776
|
-
handoff: "", handoffUpdatedAt: "",
|
|
1983
|
+
handoff: "", handoffUpdatedAt: "", handoffAudit: null,
|
|
1777
1984
|
handoffReadAt: "",
|
|
1778
1985
|
handoffHistory: [],
|
|
1779
1986
|
statusLog: [],
|
|
@@ -2245,11 +2452,24 @@ export default function planPiExtension(pi) {
|
|
|
2245
2452
|
requirementIds: linkedRequirementIds,
|
|
2246
2453
|
};
|
|
2247
2454
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
2455
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
2456
|
+
const project = await st.loadProject();
|
|
2457
|
+
const projectGuidelinesReadState = projectGuidelinesReadStateForSession(project, plannerSessionId);
|
|
2458
|
+
const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
|
|
2459
|
+
const phaseRef = formatPhaseRef(phase.number, featureNumberOfPhase(phase, features));
|
|
2460
|
+
const taskRef = `${phaseRef}/T${String(task.number).padStart(3, "0")}`;
|
|
2461
|
+
const featureRef = feature ? formatFeatureRef(feature.number) : "";
|
|
2462
|
+
if (!projectGuidelinesReady) {
|
|
2463
|
+
const actions = piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, true, !requirementsReady, `/planner task start ${taskRef}`);
|
|
2464
|
+
ctx.ui.notify(`❌ TASK START FAILED [PROJECT_GUIDELINES_READ_REQUIRED]\nstarted: false\nProject Guidelines must be read before task work. Current state: ${projectGuidelinesReadState}.\nNext required actions:\n${actions.map((action, index) => `${index + 1}. ${action}`).join("\n")}`, "error");
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2248
2467
|
if (!contextEligibility.eligible) {
|
|
2249
|
-
|
|
2468
|
+
const actions = piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, false, !requirementsReady, `/planner task start ${taskRef}`);
|
|
2469
|
+
ctx.ui.notify(`❌ TASK START FAILED [CONTEXT_READ_REQUIRED]\nstarted: false\n${contextEligibility.reason}\nNext required actions:\n${actions.map((action, index) => `${index + 1}. ${action}`).join("\n")}`, "error");
|
|
2250
2470
|
return;
|
|
2251
2471
|
}
|
|
2252
|
-
if (!
|
|
2472
|
+
if (!requirementsReady) {
|
|
2253
2473
|
ctx.ui.notify("❌ TASK START FAILED [REQUIREMENTS_READ_REQUIRED]\nstarted: false\nRun requirement_list, then retry /planner task start. The task remains unchanged.", "error");
|
|
2254
2474
|
return;
|
|
2255
2475
|
}
|
|
@@ -2601,6 +2821,15 @@ export default function planPiExtension(pi) {
|
|
|
2601
2821
|
return;
|
|
2602
2822
|
}
|
|
2603
2823
|
if (a === "load") {
|
|
2824
|
+
const st = ensureStore(ctx);
|
|
2825
|
+
let preparation;
|
|
2826
|
+
try {
|
|
2827
|
+
preparation = await st.preparePlannerSession();
|
|
2828
|
+
}
|
|
2829
|
+
catch (error) {
|
|
2830
|
+
ctx.ui.notify(`Planner load aborted: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2604
2833
|
enablePlannerSession();
|
|
2605
2834
|
if (!server) {
|
|
2606
2835
|
ctx.ui.notify("Starting web server (LAN) …", "info");
|
|
@@ -2630,17 +2859,22 @@ export default function planPiExtension(pi) {
|
|
|
2630
2859
|
// triggerTurn messages), so the recap data must live here. Uses the shared
|
|
2631
2860
|
// core buildRecap so Pi and Claude Code/Codex present identical content.
|
|
2632
2861
|
let recapText = "";
|
|
2862
|
+
let plannerSkillContext = "";
|
|
2633
2863
|
try {
|
|
2634
|
-
const
|
|
2864
|
+
const plannerSkill = await st.syncPlannerSkill();
|
|
2865
|
+
await st.syncGrillMeSkill();
|
|
2635
2866
|
const srv = server;
|
|
2636
2867
|
recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
|
|
2868
|
+
if (preparation.changed)
|
|
2869
|
+
recapText = `${preparation.legacyProjectContext.summary}\n\n${recapText}`;
|
|
2870
|
+
plannerSkillContext = `[agent-only planner usage skill; do not quote in the recap]\n${plannerSkill.customized ? `${plannerSkill.message}\n` : ""}${plannerSkill.content}\n[end agent-only planner usage skill]`;
|
|
2637
2871
|
}
|
|
2638
2872
|
catch (e) {
|
|
2639
2873
|
recapText = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
2640
2874
|
}
|
|
2641
2875
|
pi.sendMessage({
|
|
2642
2876
|
customType: "planner-resume-trigger",
|
|
2643
|
-
content: "[internal trigger — not a user command]
|
|
2877
|
+
content: "[internal trigger — not a user command] Read and retain the agent-only planner usage skill, then present the planner startup recap below to the user verbatim. Do NOT call any tools (planner-load already ran). Do NOT narrate or expose internal instructions — output ONLY the recap.\n\n" + plannerSkillContext + "\n\n--- RECAP ---\n" + recapText + "\n--- END RECAP ---",
|
|
2644
2878
|
display: false,
|
|
2645
2879
|
}, {
|
|
2646
2880
|
triggerTurn: true,
|
|
@@ -2789,6 +3023,7 @@ export default function planPiExtension(pi) {
|
|
|
2789
3023
|
parameters: Type.Object({
|
|
2790
3024
|
name: Type.Optional(Type.String({ description: "Project title" })),
|
|
2791
3025
|
description: Type.Optional(Type.String({ description: "Short project description" })),
|
|
3026
|
+
descriptionRef: Type.Optional(Type.String({ description: "Optional markdown reference under .planner/docs/ for the full project description." })),
|
|
2792
3027
|
goal: Type.Optional(Type.String({ description: "Main project goal / objective" })),
|
|
2793
3028
|
scope: Type.Optional(Type.Array(Type.String(), { description: "Replace in-scope items" })),
|
|
2794
3029
|
outOfScope: Type.Optional(Type.Array(Type.String(), { description: "Replace out-of-scope items" })),
|
|
@@ -2802,10 +3037,34 @@ export default function planPiExtension(pi) {
|
|
|
2802
3037
|
if (!st)
|
|
2803
3038
|
return { content: [{ type: "text", text: "No .planner/ found. Use plan_init first." }], details: {} };
|
|
2804
3039
|
const project = await st.loadProject();
|
|
3040
|
+
const mutableFields = ["name", "description", "descriptionRef", "goal", "scope", "outOfScope", "technologies", "tools", "globalRules", "decisions"];
|
|
3041
|
+
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
3042
|
+
if (receivedFields.length === 0) {
|
|
3043
|
+
return mutationNoFieldsFailure({
|
|
3044
|
+
entity: "project",
|
|
3045
|
+
ref: project.name,
|
|
3046
|
+
operation: "update",
|
|
3047
|
+
mutableFields,
|
|
3048
|
+
retryCommand: "project_update with a concise description summary and the intended metadata",
|
|
3049
|
+
readBackCommand: "plan_get",
|
|
3050
|
+
descriptionFallback: {
|
|
3051
|
+
inputField: "description",
|
|
3052
|
+
storedField: "project description",
|
|
3053
|
+
descriptionRefField: "descriptionRef",
|
|
3054
|
+
},
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
2805
3057
|
if (params.name !== undefined)
|
|
2806
3058
|
project.name = params.name.trim();
|
|
2807
3059
|
if (params.description !== undefined)
|
|
2808
3060
|
project.description = params.description.trim();
|
|
3061
|
+
if (params.descriptionRef !== undefined) {
|
|
3062
|
+
const descriptionRef = normalizeDescriptionRef(params.descriptionRef);
|
|
3063
|
+
if (descriptionRef)
|
|
3064
|
+
project.descriptionRef = descriptionRef;
|
|
3065
|
+
else
|
|
3066
|
+
delete project.descriptionRef;
|
|
3067
|
+
}
|
|
2809
3068
|
if (params.goal !== undefined)
|
|
2810
3069
|
project.goal = params.goal.trim();
|
|
2811
3070
|
if (params.scope !== undefined)
|
|
@@ -2822,7 +3081,95 @@ export default function planPiExtension(pi) {
|
|
|
2822
3081
|
project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
|
|
2823
3082
|
await st.saveProject(project);
|
|
2824
3083
|
await st.writeGenerated();
|
|
2825
|
-
return { content: [{ type: "text", text: `Project updated: ${project.name}
|
|
3084
|
+
return { content: [{ type: "text", text: `Project updated: ${project.name}. Fields saved: ${receivedFields.join(", ")}.` }], details: { ...project, updated: true, updatedFields: receivedFields } };
|
|
3085
|
+
},
|
|
3086
|
+
});
|
|
3087
|
+
pi.registerTool({
|
|
3088
|
+
name: "project_guidelines_show",
|
|
3089
|
+
label: "Project Guidelines Show",
|
|
3090
|
+
description: "Read the canonical Project Guidelines / Coding Standard section for this project. Call this when task_start or task_switch nextActions require it, and keep the guidance in working memory while doing task work.",
|
|
3091
|
+
parameters: Type.Object({}),
|
|
3092
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
3093
|
+
const st = await requirePlan(ctx);
|
|
3094
|
+
if (!st)
|
|
3095
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3096
|
+
const project = await st.loadProject();
|
|
3097
|
+
if (project.projectGuidelines.content.trim()) {
|
|
3098
|
+
await st.recordProjectGuidelinesRead({ sessionId: plannerSessionId });
|
|
3099
|
+
}
|
|
3100
|
+
const refreshed = await st.loadProject();
|
|
3101
|
+
const content = refreshed.projectGuidelines.content.trim();
|
|
3102
|
+
return {
|
|
3103
|
+
content: [{ type: "text", text: content
|
|
3104
|
+
? [
|
|
3105
|
+
"## Project Guidelines",
|
|
3106
|
+
content,
|
|
3107
|
+
].join("\n\n")
|
|
3108
|
+
: "No project guidelines set."
|
|
3109
|
+
}],
|
|
3110
|
+
details: {
|
|
3111
|
+
projectGuidelines: refreshed.projectGuidelines,
|
|
3112
|
+
readRecorded: Boolean(content),
|
|
3113
|
+
},
|
|
3114
|
+
};
|
|
3115
|
+
},
|
|
3116
|
+
});
|
|
3117
|
+
pi.registerTool({
|
|
3118
|
+
name: "project_guidelines_update",
|
|
3119
|
+
label: "Project Guidelines Update",
|
|
3120
|
+
description: "Replace the canonical Project Guidelines / Coding Standard text that agents must read on load and before starting or switching task work.",
|
|
3121
|
+
parameters: Type.Object({
|
|
3122
|
+
content: Type.String({ description: "Full Project Guidelines markdown/text. Pass an empty string to clear the section." }),
|
|
3123
|
+
}),
|
|
3124
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3125
|
+
const st = await requirePlan(ctx);
|
|
3126
|
+
if (!st)
|
|
3127
|
+
return { content: [{ type: "text", text: "No .planner/ found. Use plan_init first." }], details: {} };
|
|
3128
|
+
const project = await st.loadProject();
|
|
3129
|
+
project.projectGuidelines = {
|
|
3130
|
+
content: params.content.trim(),
|
|
3131
|
+
updatedAt: project.projectGuidelines.updatedAt,
|
|
3132
|
+
sessionInfo: project.projectGuidelines.sessionInfo,
|
|
3133
|
+
};
|
|
3134
|
+
await st.saveProject(project);
|
|
3135
|
+
await st.writeGenerated();
|
|
3136
|
+
const refreshed = await st.loadProject();
|
|
3137
|
+
return {
|
|
3138
|
+
content: [{ type: "text", text: refreshed.projectGuidelines.content
|
|
3139
|
+
? `Project Guidelines updated. Updated at ${refreshed.projectGuidelines.updatedAt}.`
|
|
3140
|
+
: "Project Guidelines cleared."
|
|
3141
|
+
}],
|
|
3142
|
+
details: {
|
|
3143
|
+
updated: true,
|
|
3144
|
+
projectGuidelines: refreshed.projectGuidelines,
|
|
3145
|
+
},
|
|
3146
|
+
};
|
|
3147
|
+
},
|
|
3148
|
+
});
|
|
3149
|
+
pi.registerTool({
|
|
3150
|
+
name: "project_context_migrate",
|
|
3151
|
+
label: "Project Context Migrate",
|
|
3152
|
+
description: "Preview or explicitly apply the lossless migration of legacy project globalRules/workflowRules into Project Guidelines and legacy decisions into structured accepted decisions. Ordinary planner loads never run this migration.",
|
|
3153
|
+
parameters: Type.Object({
|
|
3154
|
+
apply: Type.Optional(Type.Boolean({ description: "False previews without mutation. True explicitly applies the previewed migration and clears only the migrated legacy fields." })),
|
|
3155
|
+
}),
|
|
3156
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3157
|
+
const st = await requirePlan(ctx);
|
|
3158
|
+
if (!st)
|
|
3159
|
+
return { content: [{ type: "text", text: "No .planner/ found. Use plan_init first." }], details: {} };
|
|
3160
|
+
const preview = await st.previewLegacyProjectContextMigration();
|
|
3161
|
+
if (params.apply !== true) {
|
|
3162
|
+
return {
|
|
3163
|
+
content: [{ type: "text", text: ["Project context migration preview (no changes applied).", legacyContextMigrationSummary(preview), preview.hasLegacyContext ? "Retry with apply=true to confirm." : "No migration is needed."].join("\n") }],
|
|
3164
|
+
details: { applied: false, preview },
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
const result = await st.migrateLegacyProjectContext();
|
|
3168
|
+
await st.writeGenerated();
|
|
3169
|
+
return {
|
|
3170
|
+
content: [{ type: "text", text: [result.applied ? "Legacy project context migration applied and verified." : "No legacy project context required migration.", legacyContextMigrationSummary(result.preview)].join("\n") }],
|
|
3171
|
+
details: result,
|
|
3172
|
+
};
|
|
2826
3173
|
},
|
|
2827
3174
|
});
|
|
2828
3175
|
pi.registerTool({
|
|
@@ -2851,6 +3198,11 @@ export default function planPiExtension(pi) {
|
|
|
2851
3198
|
description: Type.Optional(Type.String({ description: "Requirement description" })),
|
|
2852
3199
|
status: Type.Optional(Type.String({ description: "Initial status: planned|in-progress|done|blocked|canceled" })),
|
|
2853
3200
|
linkedPhaseIds: Type.Optional(Type.Array(Type.String(), { description: "Optional linked phase IDs" })),
|
|
3201
|
+
macroTasks: Type.Optional(Type.Array(Type.Object({
|
|
3202
|
+
title: Type.String({ description: "Macro-task title" }),
|
|
3203
|
+
description: Type.Optional(Type.String({ description: "Macro-task description" })),
|
|
3204
|
+
status: Type.Optional(Type.String({ description: "Macro-task status" })),
|
|
3205
|
+
}), { description: "Initial macro tasks. IDs and timestamps are planner-managed." })),
|
|
2854
3206
|
}),
|
|
2855
3207
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2856
3208
|
const st = await requirePlan(ctx);
|
|
@@ -2864,19 +3216,37 @@ export default function planPiExtension(pi) {
|
|
|
2864
3216
|
return { content: [{ type: "text", text: links.error }], details: {} };
|
|
2865
3217
|
const requirements = await st.loadRequirements();
|
|
2866
3218
|
const now = nowISO();
|
|
3219
|
+
let macroTasks;
|
|
3220
|
+
try {
|
|
3221
|
+
macroTasks = reconcileRequirementMacroTasks([], (params.macroTasks ?? []).map((task) => ({
|
|
3222
|
+
title: task.title,
|
|
3223
|
+
...(task.description !== undefined ? { description: task.description } : {}),
|
|
3224
|
+
status: task.status?.trim() ?? "planned",
|
|
3225
|
+
})), now);
|
|
3226
|
+
}
|
|
3227
|
+
catch (error) {
|
|
3228
|
+
const message = error instanceof RequirementMacroTaskError ? error.message : "Invalid macro tasks.";
|
|
3229
|
+
return { content: [{ type: "text", text: message }], details: {} };
|
|
3230
|
+
}
|
|
2867
3231
|
const requirement = {
|
|
2868
3232
|
id: createRequirementId(),
|
|
2869
3233
|
title,
|
|
2870
3234
|
description: params.description?.trim() ?? "",
|
|
2871
3235
|
status: params.status?.trim() ?? "planned",
|
|
2872
|
-
macroTasks
|
|
3236
|
+
macroTasks,
|
|
2873
3237
|
linkedPhaseIds: links.linkedPhaseIds,
|
|
2874
3238
|
sessionInfo: [],
|
|
2875
3239
|
createdAt: now,
|
|
2876
3240
|
updatedAt: now,
|
|
2877
3241
|
};
|
|
2878
3242
|
requirements.requirements.push(requirement);
|
|
2879
|
-
|
|
3243
|
+
try {
|
|
3244
|
+
await st.saveRequirements(requirements);
|
|
3245
|
+
}
|
|
3246
|
+
catch (error) {
|
|
3247
|
+
const message = error instanceof RequirementMacroTaskError ? error.message : error instanceof Error ? error.message : "Invalid requirement.";
|
|
3248
|
+
return { content: [{ type: "text", text: message }], details: {} };
|
|
3249
|
+
}
|
|
2880
3250
|
await st.writeGenerated();
|
|
2881
3251
|
return { content: [{ type: "text", text: `Requirement created: ${requirement.id}` }], details: requirement };
|
|
2882
3252
|
},
|
|
@@ -2891,6 +3261,12 @@ export default function planPiExtension(pi) {
|
|
|
2891
3261
|
description: Type.Optional(Type.String({ description: "Requirement description" })),
|
|
2892
3262
|
status: Type.Optional(Type.String({ description: "Status: planned|in-progress|done|blocked|canceled" })),
|
|
2893
3263
|
linkedPhaseIds: Type.Optional(Type.Array(Type.String(), { description: "Replace linked phase IDs" })),
|
|
3264
|
+
macroTasks: Type.Optional(Type.Array(Type.Object({
|
|
3265
|
+
id: Type.Optional(Type.String({ description: "Existing planner-assigned macro-task ID; omit for a new item." })),
|
|
3266
|
+
title: Type.String({ description: "Macro-task title" }),
|
|
3267
|
+
description: Type.Optional(Type.String({ description: "Macro-task description" })),
|
|
3268
|
+
status: Type.String({ description: "Macro-task status" }),
|
|
3269
|
+
}), { description: "Replace, add, remove, or reorder macro tasks. IDs/timestamps are preserved or assigned by the planner." })),
|
|
2894
3270
|
}),
|
|
2895
3271
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2896
3272
|
const st = await requirePlan(ctx);
|
|
@@ -2916,7 +3292,22 @@ export default function planPiExtension(pi) {
|
|
|
2916
3292
|
return { content: [{ type: "text", text: links.error }], details: {} };
|
|
2917
3293
|
requirement.linkedPhaseIds = links.linkedPhaseIds;
|
|
2918
3294
|
}
|
|
2919
|
-
|
|
3295
|
+
const now = nowISO();
|
|
3296
|
+
if (params.macroTasks !== undefined) {
|
|
3297
|
+
try {
|
|
3298
|
+
requirement.macroTasks = reconcileRequirementMacroTasks(requirement.macroTasks, params.macroTasks.map((task) => ({
|
|
3299
|
+
...(task.id ? { id: task.id } : {}),
|
|
3300
|
+
title: task.title,
|
|
3301
|
+
...(task.description !== undefined ? { description: task.description } : {}),
|
|
3302
|
+
status: task.status.trim(),
|
|
3303
|
+
})), now);
|
|
3304
|
+
}
|
|
3305
|
+
catch (error) {
|
|
3306
|
+
const message = error instanceof RequirementMacroTaskError ? error.message : "Invalid macro tasks.";
|
|
3307
|
+
return { content: [{ type: "text", text: message }], details: {} };
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
requirement.updatedAt = now;
|
|
2920
3311
|
await st.saveRequirements(requirements);
|
|
2921
3312
|
await st.writeGenerated();
|
|
2922
3313
|
return { content: [{ type: "text", text: `Requirement updated: ${requirement.id}` }], details: requirement };
|
|
@@ -2943,6 +3334,77 @@ export default function planPiExtension(pi) {
|
|
|
2943
3334
|
return { content: [{ type: "text", text: `Requirement deleted: ${params.requirementId}` }], details: { deleted: params.requirementId } };
|
|
2944
3335
|
},
|
|
2945
3336
|
});
|
|
3337
|
+
pi.registerTool({
|
|
3338
|
+
name: "idea_list", label: "Idea List", description: "List rollup-independent Ideas Inbox entries.", parameters: Type.Object({}),
|
|
3339
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) { const st = await requirePlan(ctx); if (!st)
|
|
3340
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} }; const ideas = (await st.loadIdeas()).ideas; return { content: [{ type: "text", text: ideas.map((idea) => `- ${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}${idea.promotion ? ` → ${idea.promotion.targetRef}` : ""}`).join("\n") || "No ideas" }], details: { ideas } }; },
|
|
3341
|
+
});
|
|
3342
|
+
pi.registerTool({
|
|
3343
|
+
name: "idea_show", label: "Idea Show", description: "Show an idea by I-number, short ID, UUID, or title.", parameters: Type.Object({ idea: Type.String() }),
|
|
3344
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { const st = await requirePlan(ctx); if (!st)
|
|
3345
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} }; const idea = findIdeaByRef((await st.loadIdeas()).ideas, params.idea); return idea ? { content: [{ type: "text", text: `${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}\n\n${idea.description || "(no description)"}` }], details: { idea } } : { content: [{ type: "text", text: `Idea not found: ${params.idea}` }], details: { errorCode: "NOT_FOUND" } }; },
|
|
3346
|
+
});
|
|
3347
|
+
pi.registerTool({
|
|
3348
|
+
name: "idea_create", label: "Idea Create", description: "Create an independent Ideas Inbox entry.", parameters: Type.Object({ title: Type.String(), description: Type.Optional(Type.String()) }),
|
|
3349
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { const st = await requirePlan(ctx); if (!st)
|
|
3350
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} }; const idea = await st.createIdea({ title: params.title, ...(params.description !== undefined ? { description: params.description } : {}) }); await st.writeGenerated(); return { content: [{ type: "text", text: `Idea created: ${formatIdeaRef(idea.number)} · ${idea.shortId} — ${idea.title}` }], details: { idea, created: true } }; },
|
|
3351
|
+
});
|
|
3352
|
+
pi.registerTool({
|
|
3353
|
+
name: "idea_update", label: "Idea Update", description: "Update an Ideas Inbox title or description.", parameters: Type.Object({ idea: Type.String(), title: Type.Optional(Type.String()), description: Type.Optional(Type.String()) }),
|
|
3354
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { const st = await requirePlan(ctx); if (!st)
|
|
3355
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} }; const current = findIdeaByRef((await st.loadIdeas()).ideas, params.idea); if (!current)
|
|
3356
|
+
return { content: [{ type: "text", text: `Idea not found: ${params.idea}` }], details: { errorCode: "NOT_FOUND", updated: false } }; if (params.title === undefined && params.description === undefined)
|
|
3357
|
+
return { content: [{ type: "text", text: "No mutable idea fields were received." }], details: { errorCode: "NO_MUTABLE_FIELDS_RECEIVED", updated: false } }; const idea = await st.updateIdea(current.id, { ...(params.title !== undefined ? { title: params.title } : {}), ...(params.description !== undefined ? { description: params.description } : {}) }); await st.writeGenerated(); return { content: [{ type: "text", text: `Idea updated: ${formatIdeaRef(idea.number)} — ${idea.title}` }], details: { idea, updated: true } }; },
|
|
3358
|
+
});
|
|
3359
|
+
pi.registerTool({
|
|
3360
|
+
name: "idea_delete", label: "Idea Delete", description: "Delete an Idea only after explicit user confirmation.", parameters: Type.Object({ idea: Type.String(), confirmed: Type.Boolean() }),
|
|
3361
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { if (!params.confirmed)
|
|
3362
|
+
return { content: [{ type: "text", text: "Idea deletion requires confirmed=true after explicit user confirmation." }], details: { errorCode: "CONFIRMATION_REQUIRED", deleted: false } }; const st = await requirePlan(ctx); if (!st)
|
|
3363
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} }; const current = findIdeaByRef((await st.loadIdeas()).ideas, params.idea); if (!current)
|
|
3364
|
+
return { content: [{ type: "text", text: `Idea not found: ${params.idea}` }], details: { errorCode: "NOT_FOUND", deleted: false } }; await st.deleteIdea(current.id); await st.writeGenerated(); return { content: [{ type: "text", text: `Idea deleted: ${formatIdeaRef(current.number)}.` }], details: { deleted: true, ideaRef: formatIdeaRef(current.number) } }; },
|
|
3365
|
+
});
|
|
3366
|
+
pi.registerTool({
|
|
3367
|
+
name: "idea_promotion_begin", label: "Idea Promotion Begin", description: "Begin a grill-me idea promotion discussion without persisting a target or promotion.", parameters: Type.Object({ idea: Type.String(), targetType: Type.Union([Type.Literal("feature"), Type.Literal("phase"), Type.Literal("task")]) }),
|
|
3368
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3369
|
+
const st = await requirePlan(ctx);
|
|
3370
|
+
if (!st)
|
|
3371
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3372
|
+
const idea = findIdeaByRef((await st.loadIdeas()).ideas, params.idea);
|
|
3373
|
+
if (!idea)
|
|
3374
|
+
return { content: [{ type: "text", text: `Idea not found: ${params.idea}` }], details: { errorCode: "NOT_FOUND" } };
|
|
3375
|
+
if (idea.promotion)
|
|
3376
|
+
return { content: [{ type: "text", text: `${formatIdeaRef(idea.number)} is already promoted to ${idea.promotion.targetRef}.` }], details: { errorCode: "ALREADY_PROMOTED", idea } };
|
|
3377
|
+
const features = (await st.loadFeatures()).features;
|
|
3378
|
+
const phases = await st.loadAllPhases();
|
|
3379
|
+
const recommendedFeature = features[0];
|
|
3380
|
+
const recommendedPhase = recommendedFeature ? phases.find((phase) => phase.featureId === recommendedFeature.id) : phases[0];
|
|
3381
|
+
const recommendation = params.targetType === "feature"
|
|
3382
|
+
? "Create a new feature after the discussion."
|
|
3383
|
+
: params.targetType === "phase"
|
|
3384
|
+
? recommendedFeature ? `Recommended parent feature: ${formatFeatureRef(recommendedFeature.number)} — ${recommendedFeature.name}. Confirm or choose another feature before creating the phase.` : "Create and confirm a parent feature before promoting to a phase."
|
|
3385
|
+
: recommendedPhase ? `Recommended destination: ${formatPhaseRef(recommendedPhase.number, features.find((feature) => feature.id === recommendedPhase.featureId)?.number)} — ${recommendedPhase.title}. Confirm its feature and phase before creating the task.` : "Create and confirm a parent feature and phase before promoting to a task.";
|
|
3386
|
+
return { content: [{ type: "text", text: `Promotion discussion started for ${formatIdeaRef(idea.number)}. ${recommendation}` }], details: { idea, targetType: params.targetType, recommendation, grillMeSkill: await st.ideaDiscussionSkill(), persisted: false, nextActions: ["Follow grill-me one question at a time.", "Obtain explicit parent and target confirmation.", "Create the agreed target using feature_create, phase_create, or task_create.", "Call idea_promotion_finalize with discussionCompleted=true and confirmed=true."] } };
|
|
3387
|
+
},
|
|
3388
|
+
});
|
|
3389
|
+
pi.registerTool({
|
|
3390
|
+
name: "idea_promotion_finalize", label: "Idea Promotion Finalize", description: "Record an idea promotion after grill-me discussion, explicit confirmation, and successful normal target creation.", parameters: Type.Object({ idea: Type.String(), targetType: Type.Union([Type.Literal("feature"), Type.Literal("phase"), Type.Literal("task")]), targetRef: Type.String(), discussionCompleted: Type.Boolean(), confirmed: Type.Boolean() }),
|
|
3391
|
+
async execute(_id, params, _signal, _onUpdate, ctx) { if (!params.discussionCompleted || !params.confirmed)
|
|
3392
|
+
return { content: [{ type: "text", text: "Promotion requires discussionCompleted=true and confirmed=true; no promotion was persisted." }], details: { errorCode: "IDEA_PROMOTION_CONFIRMATION_REQUIRED", promoted: false } }; const st = await requirePlan(ctx); if (!st)
|
|
3393
|
+
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} }; const idea = findIdeaByRef((await st.loadIdeas()).ideas, params.idea); if (!idea)
|
|
3394
|
+
return { content: [{ type: "text", text: `Idea not found: ${params.idea}` }], details: { errorCode: "NOT_FOUND", promoted: false } }; const features = (await st.loadFeatures()).features; const phases = await st.loadAllPhases(); let target; if (params.targetType === "feature") {
|
|
3395
|
+
const resolved = resolveFeatureRefStrict(features, params.targetRef);
|
|
3396
|
+
target = resolved.ok ? { id: resolved.feature.id } : undefined;
|
|
3397
|
+
}
|
|
3398
|
+
else if (params.targetType === "phase") {
|
|
3399
|
+
const phase = findPhaseByRef(phases, features, params.targetRef);
|
|
3400
|
+
target = phase ? { id: phase.id } : undefined;
|
|
3401
|
+
}
|
|
3402
|
+
else {
|
|
3403
|
+
const found = findTaskByRef(phases, features, params.targetRef);
|
|
3404
|
+
target = found ? { id: found.task.id } : undefined;
|
|
3405
|
+
} if (!target)
|
|
3406
|
+
return { content: [{ type: "text", text: `Confirmed ${params.targetType} target not found: ${params.targetRef}; no promotion was persisted.` }], details: { errorCode: "TARGET_NOT_FOUND", promoted: false } }; const promoted = await st.promoteIdea(idea.id, { targetType: params.targetType, targetId: target.id }); await st.writeGenerated(); return { content: [{ type: "text", text: `Idea promoted: ${formatIdeaRef(promoted.number)} → ${promoted.promotion.targetRef}` }], details: { idea: promoted, promoted: true, targetRef: promoted.promotion.targetRef } }; },
|
|
3407
|
+
});
|
|
2946
3408
|
pi.registerTool({
|
|
2947
3409
|
name: "plan_get",
|
|
2948
3410
|
label: "Plan Get",
|
|
@@ -3041,9 +3503,11 @@ export default function planPiExtension(pi) {
|
|
|
3041
3503
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3042
3504
|
const list = await st.listHandoffs();
|
|
3043
3505
|
if (list.length === 0)
|
|
3044
|
-
return { content: [{ type: "text", text: "⚠️ plan_get_handoff is deprecated — no phase handoffs set. Use handoff_list / handoff_show (entity-scoped, phase.handoff field)." }], details: { deprecated: true, count: 0 } };
|
|
3045
|
-
const
|
|
3046
|
-
|
|
3506
|
+
return { content: [{ type: "text", text: "⚠️ plan_get_handoff is deprecated — no phase handoffs set. Use handoff_list / handoff_show (entity-scoped, phase.handoff field)." }], details: { deprecated: true, count: 0, total: 0, page: 1, totalPages: 1, handoffs: [] } };
|
|
3507
|
+
const result = boundedPage(list, 1, 10);
|
|
3508
|
+
const { items, ...pageInfo } = result;
|
|
3509
|
+
const lines = items.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt}; ${e.contentLength} chars)`);
|
|
3510
|
+
return { content: [{ type: "text", text: `⚠️ plan_get_handoff is deprecated — use handoff_list / handoff_show. Page 1/${pageInfo.totalPages} (${pageInfo.total} total):\n${lines.join("\n")}` }], details: { deprecated: true, count: items.length, ...pageInfo, handoffs: items } };
|
|
3047
3511
|
},
|
|
3048
3512
|
});
|
|
3049
3513
|
pi.registerTool({
|
|
@@ -3134,17 +3598,22 @@ export default function planPiExtension(pi) {
|
|
|
3134
3598
|
pi.registerTool({
|
|
3135
3599
|
name: "handoff_list",
|
|
3136
3600
|
label: "Handoff List",
|
|
3137
|
-
description: "List
|
|
3138
|
-
parameters: Type.Object({
|
|
3139
|
-
|
|
3601
|
+
description: "List phases with active entity-scoped handoffs as compact summary-only pages. Full bodies are never embedded; call handoff_show for one exact phase.",
|
|
3602
|
+
parameters: Type.Object({
|
|
3603
|
+
page: Type.Optional(Type.Integer({ minimum: 1, description: "1-based page number (default 1)." })),
|
|
3604
|
+
pageSize: Type.Optional(Type.Integer({ minimum: 1, maximum: 25, description: "Summaries per page (default 10, maximum 25)." })),
|
|
3605
|
+
}),
|
|
3606
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3140
3607
|
const st = await requirePlan(ctx);
|
|
3141
3608
|
if (!st)
|
|
3142
3609
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3143
3610
|
const list = await st.listHandoffs();
|
|
3144
3611
|
if (list.length === 0)
|
|
3145
|
-
return { content: [{ type: "text", text: "No phase handoffs set." }], details: { count: 0 } };
|
|
3146
|
-
const
|
|
3147
|
-
|
|
3612
|
+
return { content: [{ type: "text", text: "No phase handoffs set." }], details: { count: 0, total: 0, page: 1, totalPages: 1, handoffs: [] } };
|
|
3613
|
+
const result = boundedPage(list, params.page ?? 1, params.pageSize ?? 10);
|
|
3614
|
+
const { items, ...pageInfo } = result;
|
|
3615
|
+
const lines = items.map((e) => `- ${e.compositeRef} — ${e.firstLine} (updated ${e.updatedAt}; ${e.contentLength} chars)`);
|
|
3616
|
+
return { content: [{ type: "text", text: `Phase handoffs — page ${pageInfo.page}/${pageInfo.totalPages} (${pageInfo.total} total):\n${lines.join("\n")}` }], details: { count: items.length, ...pageInfo, handoffs: items } };
|
|
3148
3617
|
},
|
|
3149
3618
|
});
|
|
3150
3619
|
pi.registerTool({
|
|
@@ -3159,16 +3628,20 @@ export default function planPiExtension(pi) {
|
|
|
3159
3628
|
const r = await resolvePhaseForHandoff(st, params.phaseRef);
|
|
3160
3629
|
if (!r.ok)
|
|
3161
3630
|
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
3162
|
-
const
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3631
|
+
const phase = await st.loadPhase(r.phase.id);
|
|
3632
|
+
const bounded = boundedHandoffForTransport(phase.handoff);
|
|
3633
|
+
if (!bounded.content.trim())
|
|
3634
|
+
return { content: [{ type: "text", text: `No handoff set on ${r.compositeRef}.` }], details: { phaseRef: r.compositeRef, phaseId: r.phase.id, content: "", empty: true, handoffAudit: phase.handoffAudit } };
|
|
3635
|
+
return {
|
|
3636
|
+
content: [{ type: "text", text: `Handoff for ${r.compositeRef}:\n\n${bounded.content}` }],
|
|
3637
|
+
details: { phaseRef: r.compositeRef, phaseId: r.phase.id, ...bounded, handoffAudit: phase.handoffAudit },
|
|
3638
|
+
};
|
|
3166
3639
|
},
|
|
3167
3640
|
});
|
|
3168
3641
|
pi.registerTool({
|
|
3169
3642
|
name: "handoff_prepare",
|
|
3170
3643
|
label: "Handoff Prepare",
|
|
3171
|
-
description: "Audit one exact phase before writing a handoff. Returns the current handoff
|
|
3644
|
+
description: "Audit one exact phase before writing a handoff. Returns the current handoff token, missing task evidence, the required versioned completeness categories, and the 24,000-character inline budget. Reconcile everything through handoff_write.",
|
|
3172
3645
|
parameters: Type.Object({ phaseRef: Type.String({ description: "Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title." }) }),
|
|
3173
3646
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3174
3647
|
const st = await requirePlan(ctx);
|
|
@@ -3179,8 +3652,10 @@ export default function planPiExtension(pi) {
|
|
|
3179
3652
|
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
3180
3653
|
try {
|
|
3181
3654
|
const audit = await st.preparePhaseHandoff(r.phase.id);
|
|
3655
|
+
const { handoff, ...auditDetails } = audit;
|
|
3656
|
+
const boundedExisting = boundedHandoffForTransport(handoff);
|
|
3182
3657
|
const missing = audit.missingCompletionTasks.map((task) => `- T${String(task.number).padStart(3, "0")} — ${task.title}`).join("\n") || "- None";
|
|
3183
|
-
const existing =
|
|
3658
|
+
const existing = boundedExisting.content.trim() || "(none)";
|
|
3184
3659
|
return {
|
|
3185
3660
|
content: [{ type: "text", text: [
|
|
3186
3661
|
`Handoff preparation audit for ${r.compositeRef}`,
|
|
@@ -3188,10 +3663,14 @@ export default function planPiExtension(pi) {
|
|
|
3188
3663
|
"Done tasks missing durable completion/verification evidence:",
|
|
3189
3664
|
missing,
|
|
3190
3665
|
"",
|
|
3666
|
+
`Required completeness audit v${audit.completenessVersion} — every category must be captured or marked not-applicable with a substantive reason:`,
|
|
3667
|
+
...audit.completenessCategories.map((entry) => `- ${entry.id} — ${entry.label}`),
|
|
3668
|
+
`Maximum canonical inline content: ${audit.maxContentChars} characters. Link extended material from .planner/docs/.`,
|
|
3669
|
+
"",
|
|
3191
3670
|
"Existing active handoff (reconcile all still-relevant content):",
|
|
3192
3671
|
existing,
|
|
3193
3672
|
].join("\n") }],
|
|
3194
|
-
details: { phaseRef: r.compositeRef, ...
|
|
3673
|
+
details: { phaseRef: r.compositeRef, ...auditDetails, existingHandoffLength: boundedExisting.fullLength, existingHandoffTruncated: boundedExisting.truncated },
|
|
3195
3674
|
};
|
|
3196
3675
|
}
|
|
3197
3676
|
catch (error) {
|
|
@@ -3203,7 +3682,7 @@ export default function planPiExtension(pi) {
|
|
|
3203
3682
|
pi.registerTool({
|
|
3204
3683
|
name: "handoff_write",
|
|
3205
3684
|
label: "Handoff Write",
|
|
3206
|
-
description: "Reconcile and refresh
|
|
3685
|
+
description: "Reconcile and refresh one active handoff with durable context synchronization and a mandatory versioned completeness audit. Run handoff_prepare first. Every category must be captured or substantively not-applicable; extended detail belongs in linked .planner/docs/ Markdown.",
|
|
3207
3686
|
parameters: Type.Object({
|
|
3208
3687
|
phaseRef: Type.String({ description: "Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title." }),
|
|
3209
3688
|
title: Type.Optional(Type.String({ description: "Meaningful handoff title summarizing the work (becomes the H1 / first line in lists)." })),
|
|
@@ -3212,6 +3691,18 @@ export default function planPiExtension(pi) {
|
|
|
3212
3691
|
markdown_content: Type.Optional(Type.String({ description: "Handoff text (markdown). Preferred over content." })),
|
|
3213
3692
|
expectedHandoffUpdatedAt: Type.Optional(Type.String({ description: "Exact handoffUpdatedAt returned by handoff_prepare; use an empty string when no handoff exists." })),
|
|
3214
3693
|
reconciledExistingHandoff: Type.Optional(Type.Boolean({ description: "Confirm that all still-relevant information from the existing handoff was retained in this single refreshed body." })),
|
|
3694
|
+
completenessAudit: Type.Optional(Type.Object({
|
|
3695
|
+
version: Type.Literal(HANDOFF_COMPLETENESS_AUDIT_VERSION),
|
|
3696
|
+
entries: Type.Array(Type.Object({
|
|
3697
|
+
category: Type.String({ description: `One required category id from handoff_prepare: ${HANDOFF_COMPLETENESS_CATEGORIES.map((entry) => entry.id).join(", ")}` }),
|
|
3698
|
+
status: Type.Union([Type.Literal("captured"), Type.Literal("not-applicable")]),
|
|
3699
|
+
detail: Type.String({ description: "Substantive captured detail or a substantive reason why the category is not applicable." }),
|
|
3700
|
+
})),
|
|
3701
|
+
})),
|
|
3702
|
+
supportingDocuments: Type.Optional(Type.Array(Type.Object({
|
|
3703
|
+
path: Type.String({ description: "Committed Markdown path under .planner/docs/." }),
|
|
3704
|
+
description: Type.String({ description: "What the document contains and why the next agent needs it." }),
|
|
3705
|
+
}))),
|
|
3215
3706
|
taskUpdates: Type.Optional(Type.Array(Type.Object({
|
|
3216
3707
|
taskRef: Type.String(),
|
|
3217
3708
|
completionSummary: Type.String(),
|
|
@@ -3280,6 +3771,8 @@ export default function planPiExtension(pi) {
|
|
|
3280
3771
|
content: body,
|
|
3281
3772
|
expectedHandoffUpdatedAt: params.expectedHandoffUpdatedAt,
|
|
3282
3773
|
reconciledExistingHandoff: params.reconciledExistingHandoff === true,
|
|
3774
|
+
...(params.completenessAudit ? { completenessAudit: params.completenessAudit } : {}),
|
|
3775
|
+
...(params.supportingDocuments ? { supportingDocuments: params.supportingDocuments } : {}),
|
|
3283
3776
|
contextSync: {
|
|
3284
3777
|
taskUpdates,
|
|
3285
3778
|
...(params.phaseUpdate ? { phaseUpdate: {
|
|
@@ -3293,9 +3786,20 @@ export default function planPiExtension(pi) {
|
|
|
3293
3786
|
},
|
|
3294
3787
|
});
|
|
3295
3788
|
await st.writeGenerated();
|
|
3296
|
-
return {
|
|
3789
|
+
return {
|
|
3790
|
+
content: [{ type: "text", text: `✅ Reconciled handoff and durable context on ${r.compositeRef}; updated ${result.updatedTaskIds.length} task(s).` }],
|
|
3791
|
+
details: {
|
|
3792
|
+
phaseRef: r.compositeRef,
|
|
3793
|
+
phaseId: r.phase.id,
|
|
3794
|
+
updatedTaskIds: result.updatedTaskIds,
|
|
3795
|
+
handoffUpdatedAt: result.handoffUpdatedAt,
|
|
3796
|
+
handoffAudit: result.handoffAudit,
|
|
3797
|
+
},
|
|
3798
|
+
};
|
|
3297
3799
|
}
|
|
3298
3800
|
catch (error) {
|
|
3801
|
+
if (error instanceof HandoffContractError)
|
|
3802
|
+
return handoffContractFailure(error);
|
|
3299
3803
|
const message = error instanceof Error ? error.message : String(error);
|
|
3300
3804
|
return { content: [{ type: "text", text: `❌ Handoff refresh denied: ${message}` }], details: { error: message } };
|
|
3301
3805
|
}
|
|
@@ -3350,7 +3854,7 @@ export default function planPiExtension(pi) {
|
|
|
3350
3854
|
pi.registerTool({
|
|
3351
3855
|
name: "feature_list",
|
|
3352
3856
|
label: "Feature List",
|
|
3353
|
-
description: "List features (compact: F00x · shortId — name (status; N phases, M tasks)). Use this to discover refs cheaply — do NOT read .planner/ files or plan_get full=true to find entities.",
|
|
3857
|
+
description: "List features (compact: F00x · shortId — name (status; priority N; N phases, M tasks)). Use this to discover refs cheaply — do NOT read .planner/ files or plan_get full=true to find entities.",
|
|
3354
3858
|
parameters: Type.Object({ featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })) }),
|
|
3355
3859
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3356
3860
|
const st = await requirePlan(ctx);
|
|
@@ -3363,7 +3867,7 @@ export default function planPiExtension(pi) {
|
|
|
3363
3867
|
const phases = await st.loadAllPhases();
|
|
3364
3868
|
const summary = features.map((f) => {
|
|
3365
3869
|
const fp = phases.filter((p) => p.featureId === f.id);
|
|
3366
|
-
return `- ${formatFeatureRef(f.number)}${f.shortId ? ` · ${f.shortId}` : ""} — ${f.name} (${f.status}; ${fp.length} phases, ${fp.reduce((t, p) => t + p.tasks.length, 0)} tasks)`;
|
|
3870
|
+
return `- ${formatFeatureRef(f.number)}${f.shortId ? ` · ${f.shortId}` : ""} — ${f.name} (${f.status}${f.priority > 0 ? `; priority ${f.priority}` : ""}; ${fp.length} phases, ${fp.reduce((t, p) => t + p.tasks.length, 0)} tasks)`;
|
|
3367
3871
|
}).join("\n");
|
|
3368
3872
|
return { content: [{ type: "text", text: summary || "No features" }], details: {} };
|
|
3369
3873
|
},
|
|
@@ -3391,10 +3895,14 @@ export default function planPiExtension(pi) {
|
|
|
3391
3895
|
const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
|
|
3392
3896
|
const linkedRequirements = await st.linkedRequirementsForFeature(feature.id);
|
|
3393
3897
|
const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases${linkedRequirements.length ? `; ${linkedRequirements.length} linked requirement${linkedRequirements.length === 1 ? "" : "s"}` : ""})`;
|
|
3898
|
+
const descriptionRefBlock = feature.descriptionRef ? `\n\nDescription reference:\n- ${feature.descriptionRef}` : "";
|
|
3394
3899
|
const requirementsBlock = linkedRequirements.length > 0
|
|
3395
3900
|
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
3396
3901
|
: "\n\nLinked requirements:\n- None linked to this feature.";
|
|
3397
|
-
return {
|
|
3902
|
+
return {
|
|
3903
|
+
content: [{ type: "text", text: params.full ? `${summary}\n\n${feature.description || ""}${descriptionRefBlock}${requirementsBlock}` : summary }],
|
|
3904
|
+
details: { feature: params.full ? { ref: formatFeatureRef(feature.number), description: feature.description, descriptionRef: feature.descriptionRef ?? "" } : undefined, linkedRequirements },
|
|
3905
|
+
};
|
|
3398
3906
|
},
|
|
3399
3907
|
});
|
|
3400
3908
|
pi.registerTool({
|
|
@@ -3457,6 +3965,7 @@ export default function planPiExtension(pi) {
|
|
|
3457
3965
|
parameters: Type.Object({
|
|
3458
3966
|
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
3459
3967
|
description: Type.Optional(Type.String({ description: "Current implementation state, scope, and goals for this feature" })),
|
|
3968
|
+
descriptionRef: Type.Optional(Type.String({ description: "Optional markdown reference under .planner/docs/ for the full feature description." })),
|
|
3460
3969
|
workDone: Type.Optional(Type.String({ description: "What is already implemented / decided" })),
|
|
3461
3970
|
workRemaining: Type.Optional(Type.String({ description: "What still needs to be done" })),
|
|
3462
3971
|
dependencies: Type.Optional(Type.Array(Type.String(), { description: "Cross-feature or external dependencies" })),
|
|
@@ -3470,16 +3979,41 @@ export default function planPiExtension(pi) {
|
|
|
3470
3979
|
if (!resolvedFeature.ok)
|
|
3471
3980
|
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3472
3981
|
const featureId = resolvedFeature.feature.id;
|
|
3982
|
+
const mutableFields = ["description", "descriptionRef", "workDone", "workRemaining", "dependencies"];
|
|
3983
|
+
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
3984
|
+
if (receivedFields.length === 0) {
|
|
3985
|
+
return mutationNoFieldsFailure({
|
|
3986
|
+
entity: "feature",
|
|
3987
|
+
ref: formatFeatureRef(resolvedFeature.feature.number),
|
|
3988
|
+
operation: "discuss",
|
|
3989
|
+
mutableFields,
|
|
3990
|
+
retryCommand: `feature_discuss ${formatFeatureRef(resolvedFeature.feature.number)}`,
|
|
3991
|
+
readBackCommand: `feature_get ${formatFeatureRef(resolvedFeature.feature.number)} with full=true`,
|
|
3992
|
+
descriptionFallback: {
|
|
3993
|
+
entityId: featureId,
|
|
3994
|
+
inputField: "description",
|
|
3995
|
+
storedField: "feature description",
|
|
3996
|
+
descriptionRefField: "descriptionRef",
|
|
3997
|
+
},
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
3473
4000
|
const updatedDoc = await st.updateFeatures((doc) => {
|
|
3474
4001
|
const feature = doc.features.find((f) => f.id === featureId);
|
|
3475
4002
|
if (!feature)
|
|
3476
4003
|
return doc;
|
|
3477
4004
|
if (params.description !== undefined)
|
|
3478
|
-
feature.description = params.description;
|
|
4005
|
+
feature.description = params.description.trim();
|
|
4006
|
+
if (params.descriptionRef !== undefined) {
|
|
4007
|
+
const descriptionRef = normalizeDescriptionRef(params.descriptionRef);
|
|
4008
|
+
if (descriptionRef)
|
|
4009
|
+
feature.descriptionRef = descriptionRef;
|
|
4010
|
+
else
|
|
4011
|
+
delete feature.descriptionRef;
|
|
4012
|
+
}
|
|
3479
4013
|
if (params.workDone !== undefined)
|
|
3480
|
-
feature.workDone = params.workDone;
|
|
4014
|
+
feature.workDone = params.workDone.trim();
|
|
3481
4015
|
if (params.workRemaining !== undefined)
|
|
3482
|
-
feature.workRemaining = params.workRemaining;
|
|
4016
|
+
feature.workRemaining = params.workRemaining.trim();
|
|
3483
4017
|
if (params.dependencies !== undefined)
|
|
3484
4018
|
feature.dependsOn = params.dependencies.map((item) => item.trim()).filter(Boolean);
|
|
3485
4019
|
feature.discussedAt = nowISO();
|
|
@@ -3492,7 +4026,7 @@ export default function planPiExtension(pi) {
|
|
|
3492
4026
|
if (!feature)
|
|
3493
4027
|
return { content: [{ type: "text", text: `Feature not found: ${params.featureId}` }], details: {} };
|
|
3494
4028
|
await st.writeGenerated();
|
|
3495
|
-
return { content: [{ type: "text", text: `✅ Feature discussed/updated: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}
|
|
4029
|
+
return { content: [{ type: "text", text: `✅ Feature discussed/updated: ${formatFeatureRef(feature.number)} — ${feature.name}${feature.shortId ? ` · ${feature.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.` }], details: { ...feature, discussed: true, updatedFields: receivedFields } };
|
|
3496
4030
|
},
|
|
3497
4031
|
});
|
|
3498
4032
|
pi.registerTool({
|
|
@@ -3503,6 +4037,7 @@ export default function planPiExtension(pi) {
|
|
|
3503
4037
|
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
3504
4038
|
name: Type.Optional(Type.String({ description: "New name" })),
|
|
3505
4039
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
4040
|
+
descriptionRef: Type.Optional(Type.String({ description: "Optional markdown reference under .planner/docs/ for the full feature description." })),
|
|
3506
4041
|
status: Type.Optional(Type.String({ description: "New status: planned|in-progress|done|blocked|canceled. Avoid setting this directly unless you truly need an override: feature status is derived from child phases/tasks." })),
|
|
3507
4042
|
startDate: Type.Optional(Type.String({ description: "Start date (YYYY-MM-DD)" })),
|
|
3508
4043
|
endDate: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })),
|
|
@@ -3527,10 +4062,13 @@ export default function planPiExtension(pi) {
|
|
|
3527
4062
|
if (!resolvedFeature.ok)
|
|
3528
4063
|
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3529
4064
|
const featureId = resolvedFeature.feature.id;
|
|
4065
|
+
if (params.status !== undefined) {
|
|
4066
|
+
return derivedStatusReadOnlyResult("feature", formatFeatureRef(resolvedFeature.feature.number), String(params.status), resolvedFeature.feature.status);
|
|
4067
|
+
}
|
|
3530
4068
|
const mutableFields = [
|
|
3531
4069
|
"name",
|
|
3532
4070
|
"description",
|
|
3533
|
-
"
|
|
4071
|
+
"descriptionRef",
|
|
3534
4072
|
"startDate",
|
|
3535
4073
|
"endDate",
|
|
3536
4074
|
"workDone",
|
|
@@ -3540,13 +4078,20 @@ export default function planPiExtension(pi) {
|
|
|
3540
4078
|
];
|
|
3541
4079
|
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
3542
4080
|
if (receivedFields.length === 0) {
|
|
3543
|
-
return {
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
4081
|
+
return mutationNoFieldsFailure({
|
|
4082
|
+
entity: "feature",
|
|
4083
|
+
ref: formatFeatureRef(resolvedFeature.feature.number),
|
|
4084
|
+
operation: "update",
|
|
4085
|
+
mutableFields,
|
|
4086
|
+
retryCommand: `feature_update ${formatFeatureRef(resolvedFeature.feature.number)}`,
|
|
4087
|
+
readBackCommand: `feature_get ${formatFeatureRef(resolvedFeature.feature.number)} with full=true`,
|
|
4088
|
+
descriptionFallback: {
|
|
4089
|
+
entityId: featureId,
|
|
4090
|
+
inputField: "description",
|
|
4091
|
+
storedField: "feature description",
|
|
4092
|
+
descriptionRefField: "descriptionRef",
|
|
4093
|
+
},
|
|
4094
|
+
});
|
|
3550
4095
|
}
|
|
3551
4096
|
let foundDoc;
|
|
3552
4097
|
try {
|
|
@@ -3555,33 +4100,28 @@ export default function planPiExtension(pi) {
|
|
|
3555
4100
|
if (!feature)
|
|
3556
4101
|
return doc; // not found: no-op, handled below
|
|
3557
4102
|
if (params.name !== undefined)
|
|
3558
|
-
feature.name = params.name;
|
|
4103
|
+
feature.name = params.name.trim();
|
|
3559
4104
|
if (params.description !== undefined)
|
|
3560
|
-
feature.description = params.description;
|
|
4105
|
+
feature.description = params.description.trim();
|
|
4106
|
+
if (params.descriptionRef !== undefined) {
|
|
4107
|
+
const descriptionRef = normalizeDescriptionRef(params.descriptionRef);
|
|
4108
|
+
if (descriptionRef)
|
|
4109
|
+
feature.descriptionRef = descriptionRef;
|
|
4110
|
+
else
|
|
4111
|
+
delete feature.descriptionRef;
|
|
4112
|
+
}
|
|
3561
4113
|
if (params.workDone !== undefined)
|
|
3562
|
-
feature.workDone = params.workDone;
|
|
4114
|
+
feature.workDone = params.workDone.trim();
|
|
3563
4115
|
if (params.workRemaining !== undefined)
|
|
3564
|
-
feature.workRemaining = params.workRemaining;
|
|
4116
|
+
feature.workRemaining = params.workRemaining.trim();
|
|
3565
4117
|
if (params.startDate !== undefined)
|
|
3566
|
-
feature.startDate = params.startDate;
|
|
4118
|
+
feature.startDate = params.startDate.trim();
|
|
3567
4119
|
if (params.endDate !== undefined)
|
|
3568
|
-
feature.endDate = params.endDate;
|
|
4120
|
+
feature.endDate = params.endDate.trim();
|
|
3569
4121
|
if (params.priority !== undefined)
|
|
3570
4122
|
feature.priority = params.priority;
|
|
3571
4123
|
if (params.acceptedDecisions !== undefined)
|
|
3572
4124
|
feature.acceptedDecisions = params.acceptedDecisions;
|
|
3573
|
-
if (params.status !== undefined) {
|
|
3574
|
-
const status = params.status;
|
|
3575
|
-
if (feature.status !== status) {
|
|
3576
|
-
if (status === "in-progress" && !feature.startDate) {
|
|
3577
|
-
feature.startDate = new Date().toISOString().slice(0, 10);
|
|
3578
|
-
}
|
|
3579
|
-
if (status === "done" && !feature.endDate) {
|
|
3580
|
-
feature.endDate = new Date().toISOString().slice(0, 10);
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
feature.status = status;
|
|
3584
|
-
}
|
|
3585
4125
|
feature.updatedAt = nowISO();
|
|
3586
4126
|
return doc;
|
|
3587
4127
|
});
|
|
@@ -3643,7 +4183,7 @@ export default function planPiExtension(pi) {
|
|
|
3643
4183
|
pi.registerTool({
|
|
3644
4184
|
name: "phase_list",
|
|
3645
4185
|
label: "Phase List",
|
|
3646
|
-
description: "List phases (compact: F00x/P00x · shortId — title (status; N tasks) [F00x]). Filters: featureRef, status. Cheap discovery — do NOT read .planner/ files or plan_get full=true.",
|
|
4186
|
+
description: "List phases (compact: F00x/P00x · shortId — title (status; priority N; N tasks) [F00x]). Filters: featureRef, status. Cheap discovery — do NOT read .planner/ files or plan_get full=true.",
|
|
3647
4187
|
parameters: Type.Object({
|
|
3648
4188
|
featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })),
|
|
3649
4189
|
status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
|
|
@@ -3666,7 +4206,7 @@ export default function planPiExtension(pi) {
|
|
|
3666
4206
|
const summary = phases.map((p) => {
|
|
3667
4207
|
const fNum = featureNumberOfPhase(p, features);
|
|
3668
4208
|
const fTag = fNum !== undefined ? ` [F${pad(fNum)}]` : "";
|
|
3669
|
-
return `- ${formatPhaseRef(p.number, fNum)}${p.shortId ? ` · ${p.shortId}` : ""} — ${p.title} (${p.status}; ${p.tasks.length} tasks)${fTag}`;
|
|
4209
|
+
return `- ${formatPhaseRef(p.number, fNum)}${p.shortId ? ` · ${p.shortId}` : ""} — ${p.title} (${p.status}${p.priority > 0 ? `; priority ${p.priority}` : ""}; ${p.tasks.length} tasks)${fTag}`;
|
|
3670
4210
|
}).join("\n");
|
|
3671
4211
|
return { content: [{ type: "text", text: summary || "No phases" }], details: {} };
|
|
3672
4212
|
},
|
|
@@ -3692,10 +4232,14 @@ export default function planPiExtension(pi) {
|
|
|
3692
4232
|
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
3693
4233
|
const reqCount = linkedRequirements.length;
|
|
3694
4234
|
const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks${reqCount ? `; ${reqCount} linked requirement${reqCount === 1 ? "" : "s"}` : ""})`;
|
|
4235
|
+
const descriptionRefBlock = phase.descriptionRef ? `\n\nDescription reference:\n- ${phase.descriptionRef}` : "";
|
|
3695
4236
|
const requirementsBlock = reqCount > 0
|
|
3696
4237
|
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
3697
4238
|
: "";
|
|
3698
|
-
return {
|
|
4239
|
+
return {
|
|
4240
|
+
content: [{ type: "text", text: params.full ? `${summary}\n\n${phase.description || ""}${descriptionRefBlock}${requirementsBlock}` : summary }],
|
|
4241
|
+
details: { phase: params.full ? { ref: formatPhaseRef(phase.number, featureNumberOfPhase(phase, features)), description: phase.description, descriptionRef: phase.descriptionRef ?? "" } : undefined, linkedRequirements },
|
|
4242
|
+
};
|
|
3699
4243
|
},
|
|
3700
4244
|
});
|
|
3701
4245
|
pi.registerTool({
|
|
@@ -3746,7 +4290,7 @@ export default function planPiExtension(pi) {
|
|
|
3746
4290
|
goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
|
|
3747
4291
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
3748
4292
|
createdAt: now, updatedAt: now,
|
|
3749
|
-
handoff: "", handoffUpdatedAt: "",
|
|
4293
|
+
handoff: "", handoffUpdatedAt: "", handoffAudit: null,
|
|
3750
4294
|
handoffReadAt: "",
|
|
3751
4295
|
handoffHistory: [],
|
|
3752
4296
|
statusLog: [],
|
|
@@ -3780,6 +4324,7 @@ export default function planPiExtension(pi) {
|
|
|
3780
4324
|
status: Type.Optional(Type.String({ description: "New status: draft|discovery|planned|in-progress|done|blocked|canceled. Avoid setting this directly unless you truly need an override: phase status is derived from task statuses." })),
|
|
3781
4325
|
summary: Type.Optional(Type.String({ description: "New summary" })),
|
|
3782
4326
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
4327
|
+
descriptionRef: Type.Optional(Type.String({ description: "Optional markdown reference under .planner/docs/ for the full phase description." })),
|
|
3783
4328
|
featureId: Type.Optional(Type.String({ description: "Link/unlink phase to a feature. Use empty string to unlink." })),
|
|
3784
4329
|
priority: Type.Optional(Type.Number({ description: "Display order within the feature (lower = higher)" })),
|
|
3785
4330
|
goals: Type.Optional(Type.Array(Type.String(), { description: "Replace goals list" })),
|
|
@@ -3814,36 +4359,62 @@ export default function planPiExtension(pi) {
|
|
|
3814
4359
|
catch {
|
|
3815
4360
|
return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
|
|
3816
4361
|
}
|
|
4362
|
+
if (params.status !== undefined) {
|
|
4363
|
+
return derivedStatusReadOnlyResult("phase", formatPhaseRef(resolvedPhase.number, featureNumberOfPhase(resolvedPhase, features)), String(params.status), resolvedPhase.status);
|
|
4364
|
+
}
|
|
4365
|
+
const mutableFields = ["title", "summary", "description", "descriptionRef", "featureId", "priority", "goals", "nonGoals", "dependencies", "risks", "openQuestions", "decisions", "acceptedDecisions", "completionCriteria"];
|
|
4366
|
+
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
4367
|
+
if (receivedFields.length === 0) {
|
|
4368
|
+
return mutationNoFieldsFailure({
|
|
4369
|
+
entity: "phase",
|
|
4370
|
+
ref: formatPhaseRef(resolvedPhase.number, featureNumberOfPhase(resolvedPhase, features)),
|
|
4371
|
+
operation: "update",
|
|
4372
|
+
mutableFields,
|
|
4373
|
+
retryCommand: `phase_update ${formatPhaseRef(resolvedPhase.number, featureNumberOfPhase(resolvedPhase, features))}`,
|
|
4374
|
+
readBackCommand: `phase_get ${formatPhaseRef(resolvedPhase.number, featureNumberOfPhase(resolvedPhase, features))} with full=true`,
|
|
4375
|
+
descriptionFallback: {
|
|
4376
|
+
entityId: phaseId,
|
|
4377
|
+
inputField: "description",
|
|
4378
|
+
storedField: "phase description",
|
|
4379
|
+
descriptionRefField: "descriptionRef",
|
|
4380
|
+
},
|
|
4381
|
+
});
|
|
4382
|
+
}
|
|
3817
4383
|
const prevFeatureId = phase.featureId;
|
|
3818
4384
|
let nextFeatureIdForUpdate = prevFeatureId;
|
|
3819
4385
|
if (params.title !== undefined) {
|
|
3820
|
-
phase.title = params.title;
|
|
4386
|
+
phase.title = params.title.trim();
|
|
3821
4387
|
phase.slug = normalizeSlug(params.title);
|
|
3822
4388
|
}
|
|
3823
|
-
if (params.status !== undefined)
|
|
3824
|
-
phase.status = params.status;
|
|
3825
4389
|
if (params.summary !== undefined)
|
|
3826
|
-
phase.summary = params.summary;
|
|
4390
|
+
phase.summary = params.summary.trim();
|
|
3827
4391
|
if (params.description !== undefined)
|
|
3828
|
-
phase.description = params.description;
|
|
4392
|
+
phase.description = params.description.trim();
|
|
4393
|
+
if (params.descriptionRef !== undefined) {
|
|
4394
|
+
const descriptionRef = normalizeDescriptionRef(params.descriptionRef);
|
|
4395
|
+
if (descriptionRef)
|
|
4396
|
+
phase.descriptionRef = descriptionRef;
|
|
4397
|
+
else
|
|
4398
|
+
delete phase.descriptionRef;
|
|
4399
|
+
}
|
|
3829
4400
|
if (params.priority !== undefined)
|
|
3830
4401
|
phase.priority = params.priority;
|
|
3831
4402
|
if (params.goals !== undefined)
|
|
3832
|
-
phase.goals = params.goals;
|
|
4403
|
+
phase.goals = params.goals.map((item) => item.trim()).filter(Boolean);
|
|
3833
4404
|
if (params.nonGoals !== undefined)
|
|
3834
|
-
phase.nonGoals = params.nonGoals;
|
|
4405
|
+
phase.nonGoals = params.nonGoals.map((item) => item.trim()).filter(Boolean);
|
|
3835
4406
|
if (params.dependencies !== undefined)
|
|
3836
|
-
phase.dependencies = params.dependencies;
|
|
4407
|
+
phase.dependencies = params.dependencies.map((item) => item.trim()).filter(Boolean);
|
|
3837
4408
|
if (params.risks !== undefined)
|
|
3838
|
-
phase.risks = params.risks;
|
|
4409
|
+
phase.risks = params.risks.map((item) => item.trim()).filter(Boolean);
|
|
3839
4410
|
if (params.openQuestions !== undefined)
|
|
3840
|
-
phase.openQuestions = params.openQuestions;
|
|
4411
|
+
phase.openQuestions = params.openQuestions.map((item) => item.trim()).filter(Boolean);
|
|
3841
4412
|
if (params.decisions !== undefined)
|
|
3842
|
-
phase.decisions = params.decisions;
|
|
4413
|
+
phase.decisions = params.decisions.map((item) => item.trim()).filter(Boolean);
|
|
3843
4414
|
if (params.acceptedDecisions !== undefined)
|
|
3844
4415
|
phase.acceptedDecisions = params.acceptedDecisions;
|
|
3845
4416
|
if (params.completionCriteria !== undefined)
|
|
3846
|
-
phase.completionCriteria = params.completionCriteria;
|
|
4417
|
+
phase.completionCriteria = params.completionCriteria.map((item) => item.trim()).filter(Boolean);
|
|
3847
4418
|
if (params.featureId !== undefined) {
|
|
3848
4419
|
if (params.featureId === "") {
|
|
3849
4420
|
nextFeatureIdForUpdate = undefined;
|
|
@@ -3871,7 +4442,7 @@ export default function planPiExtension(pi) {
|
|
|
3871
4442
|
await st.savePhase(phase);
|
|
3872
4443
|
await st.writeGenerated();
|
|
3873
4444
|
const phaseUpdateFeatures = (await st.loadFeatures()).features;
|
|
3874
|
-
return { content: [{ type: "text", text: `✅ Phase updated: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, phaseUpdateFeatures))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}
|
|
4445
|
+
return { content: [{ type: "text", text: `✅ Phase updated: ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, phaseUpdateFeatures))} — ${phase.title}${phase.shortId ? ` · ${phase.shortId}` : ""}. Fields saved: ${receivedFields.join(", ")}.` }], details: { ...phase, updated: true, updatedFields: receivedFields } };
|
|
3875
4446
|
},
|
|
3876
4447
|
});
|
|
3877
4448
|
pi.registerTool({
|
|
@@ -3987,7 +4558,7 @@ export default function planPiExtension(pi) {
|
|
|
3987
4558
|
pi.registerTool({
|
|
3988
4559
|
name: "task_list",
|
|
3989
4560
|
label: "Task List",
|
|
3990
|
-
description: "List tasks (compact: F00x/P00x/T00x · shortId — title (status)). Filters: featureRef, phaseRef, status. Omit all to list every task. Cheap discovery — do NOT read .planner/ files or plan_get full=true.",
|
|
4561
|
+
description: "List tasks (compact: F00x/P00x/T00x · shortId — title (status; priority N)). Filters: featureRef, phaseRef, status. Omit all to list every task. Cheap discovery — do NOT read .planner/ files or plan_get full=true.",
|
|
3991
4562
|
parameters: Type.Object({
|
|
3992
4563
|
featureRef: Type.Optional(Type.String({})),
|
|
3993
4564
|
phaseRef: Type.Optional(Type.String({})),
|
|
@@ -4018,7 +4589,7 @@ export default function planPiExtension(pi) {
|
|
|
4018
4589
|
for (const task of phase.tasks) {
|
|
4019
4590
|
if (params.status && task.status !== params.status)
|
|
4020
4591
|
continue;
|
|
4021
|
-
out.push(`- ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${pad(task.number)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status})`);
|
|
4592
|
+
out.push(`- ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${pad(task.number)}${task.shortId ? ` · ${task.shortId}` : ""} — ${task.title} (${task.status}${task.priority > 0 ? `; priority ${task.priority}` : ""})`);
|
|
4022
4593
|
}
|
|
4023
4594
|
}
|
|
4024
4595
|
return { content: [{ type: "text", text: out.join("\n") || "No tasks" }], details: {} };
|
|
@@ -4057,6 +4628,8 @@ export default function planPiExtension(pi) {
|
|
|
4057
4628
|
const sections = [summary];
|
|
4058
4629
|
if (found.task.description?.trim())
|
|
4059
4630
|
sections.push(found.task.description.trim());
|
|
4631
|
+
if (found.task.descriptionRef)
|
|
4632
|
+
sections.push(`Description reference:\n- ${found.task.descriptionRef}`);
|
|
4060
4633
|
if (snapshot || pendingDeviation) {
|
|
4061
4634
|
sections.push([
|
|
4062
4635
|
pendingDeviation
|
|
@@ -4070,7 +4643,7 @@ export default function planPiExtension(pi) {
|
|
|
4070
4643
|
}
|
|
4071
4644
|
if (log)
|
|
4072
4645
|
sections.push(`Status log:\n${log}`);
|
|
4073
|
-
return { content: [{ type: "text", text: sections.join("\n\n") }], details: {} };
|
|
4646
|
+
return { content: [{ type: "text", text: sections.join("\n\n") }], details: { task: { ref: `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)}`, description: found.task.description, descriptionRef: found.task.descriptionRef ?? "" } } };
|
|
4074
4647
|
},
|
|
4075
4648
|
});
|
|
4076
4649
|
pi.registerTool({
|
|
@@ -4172,6 +4745,7 @@ export default function planPiExtension(pi) {
|
|
|
4172
4745
|
title: Type.Optional(Type.String({ description: "New title" })),
|
|
4173
4746
|
status: Type.Optional(Type.String({ description: "New status: planned|in-progress|done|blocked|canceled|rejected|deferred|waiting" })),
|
|
4174
4747
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
4748
|
+
descriptionRef: Type.Optional(Type.String({ description: "Optional markdown reference under .planner/docs/ for the full task description." })),
|
|
4175
4749
|
notes: Type.Optional(Type.String({ description: "New implementation notes" })),
|
|
4176
4750
|
motivation: Type.Optional(Type.String({ description: "Motivation for status change. REQUIRED when changing to blocked, canceled, rejected, deferred, waiting, or back to planned from another status." })),
|
|
4177
4751
|
decisions: Type.Optional(Type.Array(Type.String(), { description: "Replace decisions list" })),
|
|
@@ -4223,6 +4797,24 @@ export default function planPiExtension(pi) {
|
|
|
4223
4797
|
};
|
|
4224
4798
|
}
|
|
4225
4799
|
}
|
|
4800
|
+
const mutableFields = ["title", "status", "description", "descriptionRef", "notes", "motivation", "decisions", "acceptedDecisions", "priority", "checklist"];
|
|
4801
|
+
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
4802
|
+
if (receivedFields.length === 0) {
|
|
4803
|
+
return mutationNoFieldsFailure({
|
|
4804
|
+
entity: "task",
|
|
4805
|
+
ref: `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)}`,
|
|
4806
|
+
operation: "update",
|
|
4807
|
+
mutableFields,
|
|
4808
|
+
retryCommand: `task_update ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)}`,
|
|
4809
|
+
readBackCommand: `task_get ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)} with full=true`,
|
|
4810
|
+
descriptionFallback: {
|
|
4811
|
+
entityId: found.task.id,
|
|
4812
|
+
inputField: "description",
|
|
4813
|
+
storedField: "task description",
|
|
4814
|
+
descriptionRefField: "descriptionRef",
|
|
4815
|
+
},
|
|
4816
|
+
});
|
|
4817
|
+
}
|
|
4226
4818
|
try {
|
|
4227
4819
|
const updatedPhase = await st.updatePhase(hostPhase.id, (phase) => {
|
|
4228
4820
|
const task = phase.tasks.find((t) => t.id === found.task.id);
|
|
@@ -4233,11 +4825,18 @@ export default function planPiExtension(pi) {
|
|
|
4233
4825
|
if (params.priority !== undefined)
|
|
4234
4826
|
task.priority = params.priority;
|
|
4235
4827
|
if (params.description !== undefined)
|
|
4236
|
-
task.description = params.description;
|
|
4828
|
+
task.description = params.description.trim();
|
|
4829
|
+
if (params.descriptionRef !== undefined) {
|
|
4830
|
+
const descriptionRef = normalizeDescriptionRef(params.descriptionRef);
|
|
4831
|
+
if (descriptionRef)
|
|
4832
|
+
task.descriptionRef = descriptionRef;
|
|
4833
|
+
else
|
|
4834
|
+
delete task.descriptionRef;
|
|
4835
|
+
}
|
|
4237
4836
|
if (params.notes !== undefined)
|
|
4238
|
-
task.notes = params.notes;
|
|
4837
|
+
task.notes = params.notes.trim();
|
|
4239
4838
|
if (params.decisions !== undefined)
|
|
4240
|
-
task.decisions = params.decisions;
|
|
4839
|
+
task.decisions = params.decisions.map((item) => item.trim()).filter(Boolean);
|
|
4241
4840
|
if (params.acceptedDecisions !== undefined)
|
|
4242
4841
|
task.acceptedDecisions = params.acceptedDecisions;
|
|
4243
4842
|
if (params.checklist !== undefined) {
|
|
@@ -4275,7 +4874,7 @@ export default function planPiExtension(pi) {
|
|
|
4275
4874
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
|
|
4276
4875
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
4277
4876
|
await st.writeGenerated();
|
|
4278
|
-
return { content: [{ type: "text", text: `Task updated: ${updatedTask.id} (${updatedTask.status})
|
|
4877
|
+
return { content: [{ type: "text", text: `Task updated: ${updatedTask.id} (${updatedTask.status}). Fields saved: ${receivedFields.join(", ")}.` }], details: { ...updatedTask, updated: true, updatedFields: receivedFields } };
|
|
4279
4878
|
},
|
|
4280
4879
|
});
|
|
4281
4880
|
pi.registerTool({
|
|
@@ -4391,7 +4990,7 @@ export default function planPiExtension(pi) {
|
|
|
4391
4990
|
resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(), relatedTaskId: "",
|
|
4392
4991
|
pausedAt: nowISO(), pausedBy: params.paused_by?.trim() ?? "",
|
|
4393
4992
|
};
|
|
4394
|
-
invalidateReads();
|
|
4993
|
+
invalidateReads(plannerSessionId);
|
|
4395
4994
|
const checkpointed = await st.pauseTask(found.phase.id, found.task.id, snapshot);
|
|
4396
4995
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
4397
4996
|
const activeDeviation = (await st.loadProject()).workDeviations
|
|
@@ -4424,7 +5023,7 @@ export default function planPiExtension(pi) {
|
|
|
4424
5023
|
pi.registerTool({
|
|
4425
5024
|
name: "task_switch",
|
|
4426
5025
|
label: "Task Switch",
|
|
4427
|
-
description: "Switch from active work to a temporary task, even outside priority order.
|
|
5026
|
+
description: "Switch from active work to a temporary task, even outside priority order. Call this tool first so valid sessionInfo context can be reused; on denial, perform only the missing/stale reads listed in nextActions and retry. If nextActions includes project_guidelines_show, read that section and keep it in working memory while doing task work. Atomically snapshot/pause the source, start the target, and push a durable LIFO return target.",
|
|
4428
5027
|
parameters: Type.Object({
|
|
4429
5028
|
from_task: Type.String({ description: "Currently in-progress task ref" }),
|
|
4430
5029
|
to_task: Type.String({ description: "Temporary task ref to start" }),
|
|
@@ -4469,11 +5068,25 @@ export default function planPiExtension(pi) {
|
|
|
4469
5068
|
requirementIds: linkedRequirementIds,
|
|
4470
5069
|
};
|
|
4471
5070
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
5071
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
5072
|
+
const projectGuidelinesReadState = projectGuidelinesReadStateForSession(project, plannerSessionId);
|
|
5073
|
+
const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
|
|
5074
|
+
const targetPhaseRef = formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features));
|
|
5075
|
+
const targetTaskRef = `${targetPhaseRef}/T${String(target.task.number).padStart(3, "0")}`;
|
|
5076
|
+
const targetFeatureRef = targetFeature ? formatFeatureRef(targetFeature.number) : "";
|
|
5077
|
+
if (!projectGuidelinesReady) {
|
|
5078
|
+
const nextActions = piContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, true, !requirementsReady, `Retry task_switch to ${targetTaskRef}`);
|
|
5079
|
+
return {
|
|
5080
|
+
content: [{ type: "text", text: `Task switch denied: read Project Guidelines before switching task work. Current state: ${projectGuidelinesReadState}.` }],
|
|
5081
|
+
details: { projectGuidelinesReadState, contextEligibility, nextActions },
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
4472
5084
|
if (!contextEligibility.eligible) {
|
|
4473
|
-
|
|
5085
|
+
const nextActions = piContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, false, !requirementsReady, `Retry task_switch to ${targetTaskRef}`);
|
|
5086
|
+
return { content: [{ type: "text", text: `Task switch denied: context reads required. ${contextEligibility.reason}` }], details: { ...contextEligibility, nextActions } };
|
|
4474
5087
|
}
|
|
4475
|
-
if (!
|
|
4476
|
-
return { content: [{ type: "text", text: "Task switch denied: read the requirements linked to the target phase and feature before switching." }], details: { requirementIds: linkedRequirementIds } };
|
|
5088
|
+
if (!requirementsReady) {
|
|
5089
|
+
return { content: [{ type: "text", text: "Task switch denied: read the requirements linked to the target phase and feature before switching." }], details: { requirementIds: linkedRequirementIds, nextActions: ["requirement_list", `Retry task_switch to ${targetTaskRef}`] } };
|
|
4477
5090
|
}
|
|
4478
5091
|
if (!hasValidSessionAttestation(contextInput)) {
|
|
4479
5092
|
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: target.phase.id, taskId: target.task.id, ...(target.phase.featureId ? { featureId: target.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
@@ -4567,7 +5180,7 @@ export default function planPiExtension(pi) {
|
|
|
4567
5180
|
if (target.phase.id !== source.phase.id)
|
|
4568
5181
|
await st.syncTaskStatusRollup(target.phase.id);
|
|
4569
5182
|
await st.writeGenerated();
|
|
4570
|
-
invalidateReads();
|
|
5183
|
+
invalidateReads(plannerSessionId);
|
|
4571
5184
|
const sourceRef = `${formatPhaseRef(source.phase.number, featureNumberOfPhase(source.phase, features))}/T${String(source.task.number).padStart(3, "0")}`;
|
|
4572
5185
|
const targetRef = `${formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features))}/T${String(target.task.number).padStart(3, "0")}`;
|
|
4573
5186
|
return {
|
|
@@ -4584,7 +5197,7 @@ export default function planPiExtension(pi) {
|
|
|
4584
5197
|
pi.registerTool({
|
|
4585
5198
|
name: "task_start",
|
|
4586
5199
|
label: "Task Start",
|
|
4587
|
-
description: "Set a task to in-progress or resume checkpointed work.
|
|
5200
|
+
description: "Set a task to in-progress or resume checkpointed work. Call this tool before reading context so it can reuse valid sessionInfo attestations; if denied, perform only the missing/stale reads listed in nextActions, then retry. Reads may be performed in any order within the current session. If nextActions includes project_guidelines_show, read that section and keep it in working memory while doing task work. A denied start is an isError result with started=false; NEVER claim work started unless details.started is true. Success is returned only after persisted in-progress state is verified.",
|
|
4588
5201
|
parameters: Type.Object({
|
|
4589
5202
|
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
|
|
4590
5203
|
}),
|
|
@@ -4601,12 +5214,9 @@ export default function planPiExtension(pi) {
|
|
|
4601
5214
|
const taskRef = `${phaseRef}/T${String(task.number).padStart(3, "0")}`;
|
|
4602
5215
|
const feature = found.phase.featureId ? features.find((candidate) => candidate.id === found.phase.featureId) : undefined;
|
|
4603
5216
|
const featureRef = feature ? formatFeatureRef(feature.number) : "";
|
|
4604
|
-
if (task.status === "in-progress") {
|
|
4605
|
-
const outcome = taskStartSucceeded(task.id, true);
|
|
4606
|
-
return { content: [{ type: "text", text: `✅ Task already started: ${taskRef} — ${task.title} (in-progress)\nstarted: true` }], details: { ...task, ...outcome, task } };
|
|
4607
|
-
}
|
|
4608
5217
|
if (task.status === "done")
|
|
4609
|
-
return taskStartFailure(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use task_update with status=planned and a motivation to reopen ${taskRef}.`, `
|
|
5218
|
+
return taskStartFailure(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use task_update with status=planned and a motivation to reopen ${taskRef}.`, `Retry task_start ${taskRef}. If it then reports read prerequisites, perform only the missing or stale nextActions before retrying again.`], { taskId: task.id }));
|
|
5219
|
+
const project = await st.loadProject();
|
|
4610
5220
|
const linkedRequirements = [
|
|
4611
5221
|
...(await st.linkedRequirementsForPhase(found.phase.id)),
|
|
4612
5222
|
...(found.phase.featureId ? await st.linkedRequirementsForFeature(found.phase.featureId) : []),
|
|
@@ -4624,16 +5234,16 @@ export default function planPiExtension(pi) {
|
|
|
4624
5234
|
requirementIds: linkedRequirementIds,
|
|
4625
5235
|
};
|
|
4626
5236
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
5237
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
5238
|
+
const projectGuidelinesReadState = projectGuidelinesReadStateForSession(project, plannerSessionId);
|
|
5239
|
+
const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
|
|
5240
|
+
if (!projectGuidelinesReady) {
|
|
5241
|
+
return taskStartFailure(taskStartDenied("PROJECT_GUIDELINES_READ_REQUIRED", `Project Guidelines must be read before task work. Current state: ${projectGuidelinesReadState}.`, piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, true, !requirementsReady, `Retry task_start ${taskRef}`), { taskId: task.id }), { projectGuidelinesReadState, contextEligibility });
|
|
5242
|
+
}
|
|
4627
5243
|
if (!contextEligibility.eligible) {
|
|
4628
|
-
return taskStartFailure(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`,
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
...(featureRef ? [`feature_get ${featureRef} with full=true`] : []),
|
|
4632
|
-
...(linkedRequirementIds.length > 0 ? ["requirement_list"] : []),
|
|
4633
|
-
`Retry task_start ${taskRef}`,
|
|
4634
|
-
], { taskId: task.id }), { contextEligibility });
|
|
4635
|
-
}
|
|
4636
|
-
if (!hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements)) {
|
|
5244
|
+
return taskStartFailure(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, false, !requirementsReady, `Retry task_start ${taskRef}`), { taskId: task.id }), { contextEligibility });
|
|
5245
|
+
}
|
|
5246
|
+
if (!requirementsReady) {
|
|
4637
5247
|
return taskStartFailure(taskStartDenied("REQUIREMENTS_READ_REQUIRED", "Linked requirements were not read; the task remains unchanged.", ["requirement_list", `Retry task_start ${taskRef}`], { taskId: task.id, requirementIds: linkedRequirementIds }));
|
|
4638
5248
|
}
|
|
4639
5249
|
if (!hasValidSessionAttestation(contextInput)) {
|
|
@@ -4645,7 +5255,11 @@ export default function planPiExtension(pi) {
|
|
|
4645
5255
|
requirementIds: linkedRequirementIds,
|
|
4646
5256
|
});
|
|
4647
5257
|
}
|
|
4648
|
-
|
|
5258
|
+
if (task.status === "in-progress") {
|
|
5259
|
+
const outcome = taskStartSucceeded(task.id, true);
|
|
5260
|
+
return { content: [{ type: "text", text: `✅ Task already started: ${taskRef} — ${task.title} (in-progress)\nstarted: true` }], details: { ...task, ...outcome, task } };
|
|
5261
|
+
}
|
|
5262
|
+
const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume()]);
|
|
4649
5263
|
const eligibility = checkExplicitTaskStart(features, phases, task.id, project.workDeviations);
|
|
4650
5264
|
if (!eligibility.eligible)
|
|
4651
5265
|
return taskStartFailure(taskStartDenied("START_NOT_ALLOWED", eligibility.reason, ["Resolve the reported task readiness condition, then retry task_start."], { taskId: task.id }), { eligibility });
|
|
@@ -4997,10 +5611,24 @@ export default function planPiExtension(pi) {
|
|
|
4997
5611
|
const st = await requirePlan(ctx);
|
|
4998
5612
|
if (!st)
|
|
4999
5613
|
return { content: [{ type: "text", text: "No .planner/ found. Run plan_init first." }], details: { enabled: false, running: false } };
|
|
5614
|
+
let preparation;
|
|
5615
|
+
try {
|
|
5616
|
+
preparation = await st.preparePlannerSession();
|
|
5617
|
+
}
|
|
5618
|
+
catch (error) {
|
|
5619
|
+
return { content: [{ type: "text", text: `Planner load aborted: ${error instanceof Error ? error.message : String(error)}` }], details: { errorCode: "PLANNER_SESSION_PREPARATION_FAILED", enabled: false, running: false } };
|
|
5620
|
+
}
|
|
5000
5621
|
enablePlannerSession();
|
|
5001
5622
|
if (!server) {
|
|
5002
5623
|
await startServer(ctx, undefined, "lan").catch(() => { });
|
|
5003
5624
|
}
|
|
5625
|
+
const project = await st.loadProject();
|
|
5626
|
+
if (project.projectGuidelines.content.trim()) {
|
|
5627
|
+
await st.recordProjectGuidelinesRead({ sessionId: plannerSessionId });
|
|
5628
|
+
}
|
|
5629
|
+
const plannerSkill = await st.syncPlannerSkill();
|
|
5630
|
+
await st.syncGrillMeSkill();
|
|
5631
|
+
contextBlockDirty = true;
|
|
5004
5632
|
const srv = server;
|
|
5005
5633
|
let recap = "";
|
|
5006
5634
|
try {
|
|
@@ -5009,7 +5637,9 @@ export default function planPiExtension(pi) {
|
|
|
5009
5637
|
catch (e) {
|
|
5010
5638
|
recap = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
5011
5639
|
}
|
|
5012
|
-
|
|
5640
|
+
if (preparation.changed)
|
|
5641
|
+
recap = `${preparation.legacyProjectContext.summary}\n\n${recap}`;
|
|
5642
|
+
return { content: [{ type: "text", text: recap }], details: { enabled: true, running: Boolean(srv), localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort, preparation: preparation.legacyProjectContext, plannerSkill: { status: plannerSkill.status, customized: plannerSkill.customized, message: plannerSkill.message } } };
|
|
5013
5643
|
},
|
|
5014
5644
|
});
|
|
5015
5645
|
pi.registerTool({
|
|
@@ -5070,6 +5700,7 @@ export default function planPiExtension(pi) {
|
|
|
5070
5700
|
const recentActivity = activity.entries.slice(-3).reverse();
|
|
5071
5701
|
const handoffs = await st.listHandoffs();
|
|
5072
5702
|
const extensionRules = await st.extensionRules();
|
|
5703
|
+
const plannerSkill = await st.syncPlannerSkill();
|
|
5073
5704
|
const featureById = new Map(plan.features.features.map((feature) => [feature.id, feature]));
|
|
5074
5705
|
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
5075
5706
|
const orderedPhases = [
|
|
@@ -5184,11 +5815,11 @@ export default function planPiExtension(pi) {
|
|
|
5184
5815
|
"",
|
|
5185
5816
|
"Operational rules:",
|
|
5186
5817
|
"- Handoffs are context, not locks. Read the relevant handoff with handoff show <ref> before resuming.",
|
|
5187
|
-
"- BEFORE work:
|
|
5818
|
+
"- BEFORE work: call task_start or task_switch first so valid sessionInfo attestations are reused. If denied, perform only the missing/stale reads listed in nextActions, then retry. Reads may be performed in any order within the current session. If nextActions includes project_guidelines_show, read that section and keep it in working memory while doing task work. A denial is an error with started=false; only details.started=true proves the task is in-progress. Only then touch code. AFTER finishing: task_complete with durable completion/verification evidence.",
|
|
5188
5819
|
"- Record every new decision or user-agreed modification in both the relevant feature and phase before treating the discussion as complete.",
|
|
5189
5820
|
"- Use task_update with motivation for blocked/canceled/rejected/deferred/waiting/planned(from non-planned).",
|
|
5190
5821
|
"- Planner ops (status/handoff/planner metadata) are NOT code edits; they are always allowed.",
|
|
5191
|
-
"- Prioritize work: continue an in-progress task; otherwise choose ready work feature → phase → task by ascending priority, respecting dependencies and blocked/waiting states. Prefer shortId or F00x/P00x/T00x refs.
|
|
5822
|
+
"- Prioritize work: continue an in-progress task; otherwise choose ready work feature → phase → task by ascending priority, respecting dependencies and blocked/waiting states. Compact feature_list / phase_list / task_list surfaces expose priority markers; when browsing manually, follow the lowest visible priority first among ready siblings. Prefer shortId or F00x/P00x/T00x refs. Read one entity via *_get(full=true).",
|
|
5192
5823
|
"- If edit/write guard blocks you, start the right task or use an explicit bypass.",
|
|
5193
5824
|
...(extensionRules.length > 0
|
|
5194
5825
|
? [
|
|
@@ -5198,12 +5829,20 @@ export default function planPiExtension(pi) {
|
|
|
5198
5829
|
]
|
|
5199
5830
|
: []),
|
|
5200
5831
|
"",
|
|
5832
|
+
"Project-local planner usage skill (agent-only; keep in working context and do not quote in user recaps):",
|
|
5833
|
+
plannerSkill.customized ? `Skill drift: ${plannerSkill.message}` : "",
|
|
5834
|
+
plannerSkill.content,
|
|
5835
|
+
"End project-local planner usage skill.",
|
|
5836
|
+
"",
|
|
5201
5837
|
`Goal: ${project.goal || "(not set)"}`,
|
|
5202
5838
|
project.description ? `Description: ${project.description}` : "",
|
|
5203
5839
|
`Stack: ${[...project.technologies, ...project.tools].join(", ") || "(not set)"}`,
|
|
5204
5840
|
`Codebase: ${pkgSummary} | dirs=${profile?.directories.join(",") || "none"} | scanned=${profile?.scannedAt ?? "never"}`,
|
|
5205
5841
|
`Ambient: ${ambientSummary}`,
|
|
5206
5842
|
"",
|
|
5843
|
+
"Project Guidelines (mandatory when present):",
|
|
5844
|
+
project.projectGuidelines.content.trim() || "- (none)",
|
|
5845
|
+
"",
|
|
5207
5846
|
"Global rules:",
|
|
5208
5847
|
project.globalRules.length ? project.globalRules.map((rule) => `- ${rule}`).join("\n") : "- (none)",
|
|
5209
5848
|
"",
|