@agent-plan/pi-adapter 0.2.25 → 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 +685 -85
- 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-CYtuauJC.css +0 -1
- package/web-ui-dist/assets/index-DubXBudL.js +0 -318
package/dist/index.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
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";
|
|
@@ -85,14 +85,61 @@ function derivedStatusReadOnlyResult(entity, ref, attemptedStatus, effectiveStat
|
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
-
function
|
|
89
|
-
const
|
|
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) => {
|
|
90
137
|
if (read.kind === "task")
|
|
91
138
|
return [`task_get ${refs.task} with full=true`];
|
|
92
139
|
if (read.kind === "phase")
|
|
93
140
|
return [`phase_get ${refs.phase} with full=true`];
|
|
94
141
|
return refs.feature ? [`feature_get ${refs.feature} with full=true`] : [];
|
|
95
|
-
});
|
|
142
|
+
}));
|
|
96
143
|
if (requirementReadRequired)
|
|
97
144
|
actions.push("requirement_list");
|
|
98
145
|
actions.push(retryAction);
|
|
@@ -172,6 +219,8 @@ const PLANNER_COMMAND_COMPLETIONS = [
|
|
|
172
219
|
{ value: "cleanup-orphans", label: "cleanup-orphans", description: "List and remove orphan phase files" },
|
|
173
220
|
{ value: "project discuss", label: "project discuss", description: "Run project discovery" },
|
|
174
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" },
|
|
175
224
|
{ value: "feature list", label: "feature list", description: "List features" },
|
|
176
225
|
{ value: "feature add", label: "feature add", description: "Create a feature" },
|
|
177
226
|
{ value: "feature show", label: "feature show", description: "Show a feature" },
|
|
@@ -1139,6 +1188,13 @@ export default function planPiExtension(pi) {
|
|
|
1139
1188
|
"cleanup-orphans",
|
|
1140
1189
|
"project discuss",
|
|
1141
1190
|
"project language",
|
|
1191
|
+
"project guidelines",
|
|
1192
|
+
"idea list",
|
|
1193
|
+
"idea add",
|
|
1194
|
+
"idea show",
|
|
1195
|
+
"idea update",
|
|
1196
|
+
"idea delete",
|
|
1197
|
+
"idea promote",
|
|
1142
1198
|
"feature list",
|
|
1143
1199
|
"feature add",
|
|
1144
1200
|
"feature show",
|
|
@@ -1170,7 +1226,7 @@ export default function planPiExtension(pi) {
|
|
|
1170
1226
|
"load",
|
|
1171
1227
|
"stop",
|
|
1172
1228
|
];
|
|
1173
|
-
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" +
|
|
1174
1230
|
"Try: /planner <TAB> | /planner feature list | /planner task start | /planner cleanup-orphans\n" +
|
|
1175
1231
|
"Handoff actions: /planner handoff list | show P00x | write P00x | clear P00x | prepare (prepare proposes a target and asks for confirmation)";
|
|
1176
1232
|
if (!a) {
|
|
@@ -1472,7 +1528,7 @@ export default function planPiExtension(pi) {
|
|
|
1472
1528
|
// ═══════════════════════════════════════════════════════════════
|
|
1473
1529
|
if (a === "project") {
|
|
1474
1530
|
if (!b) {
|
|
1475
|
-
ctx.ui.notify("project actions: discuss | language", "info");
|
|
1531
|
+
ctx.ui.notify("project actions: discuss | language | guidelines | migrate-context", "info");
|
|
1476
1532
|
return;
|
|
1477
1533
|
}
|
|
1478
1534
|
if (b === "discuss") {
|
|
@@ -1534,7 +1590,115 @@ export default function planPiExtension(pi) {
|
|
|
1534
1590
|
ctx.ui.notify(`Saved language preferences: content=${project.contentLanguage}, chat=${project.chatLanguage}`, "info");
|
|
1535
1591
|
return;
|
|
1536
1592
|
}
|
|
1537
|
-
|
|
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");
|
|
1538
1702
|
return;
|
|
1539
1703
|
}
|
|
1540
1704
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1816,7 +1980,7 @@ export default function planPiExtension(pi) {
|
|
|
1816
1980
|
goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
|
|
1817
1981
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
1818
1982
|
createdAt: nowISO(), updatedAt: nowISO(),
|
|
1819
|
-
handoff: "", handoffUpdatedAt: "",
|
|
1983
|
+
handoff: "", handoffUpdatedAt: "", handoffAudit: null,
|
|
1820
1984
|
handoffReadAt: "",
|
|
1821
1985
|
handoffHistory: [],
|
|
1822
1986
|
statusLog: [],
|
|
@@ -2289,11 +2453,19 @@ export default function planPiExtension(pi) {
|
|
|
2289
2453
|
};
|
|
2290
2454
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
2291
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";
|
|
2292
2459
|
const phaseRef = formatPhaseRef(phase.number, featureNumberOfPhase(phase, features));
|
|
2293
2460
|
const taskRef = `${phaseRef}/T${String(task.number).padStart(3, "0")}`;
|
|
2294
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
|
+
}
|
|
2295
2467
|
if (!contextEligibility.eligible) {
|
|
2296
|
-
const actions = piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, !requirementsReady, `/planner task start ${taskRef}`);
|
|
2468
|
+
const actions = piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, false, !requirementsReady, `/planner task start ${taskRef}`);
|
|
2297
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");
|
|
2298
2470
|
return;
|
|
2299
2471
|
}
|
|
@@ -2649,6 +2821,15 @@ export default function planPiExtension(pi) {
|
|
|
2649
2821
|
return;
|
|
2650
2822
|
}
|
|
2651
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
|
+
}
|
|
2652
2833
|
enablePlannerSession();
|
|
2653
2834
|
if (!server) {
|
|
2654
2835
|
ctx.ui.notify("Starting web server (LAN) …", "info");
|
|
@@ -2678,17 +2859,22 @@ export default function planPiExtension(pi) {
|
|
|
2678
2859
|
// triggerTurn messages), so the recap data must live here. Uses the shared
|
|
2679
2860
|
// core buildRecap so Pi and Claude Code/Codex present identical content.
|
|
2680
2861
|
let recapText = "";
|
|
2862
|
+
let plannerSkillContext = "";
|
|
2681
2863
|
try {
|
|
2682
|
-
const
|
|
2864
|
+
const plannerSkill = await st.syncPlannerSkill();
|
|
2865
|
+
await st.syncGrillMeSkill();
|
|
2683
2866
|
const srv = server;
|
|
2684
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]`;
|
|
2685
2871
|
}
|
|
2686
2872
|
catch (e) {
|
|
2687
2873
|
recapText = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
2688
2874
|
}
|
|
2689
2875
|
pi.sendMessage({
|
|
2690
2876
|
customType: "planner-resume-trigger",
|
|
2691
|
-
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 ---",
|
|
2692
2878
|
display: false,
|
|
2693
2879
|
}, {
|
|
2694
2880
|
triggerTurn: true,
|
|
@@ -2837,6 +3023,7 @@ export default function planPiExtension(pi) {
|
|
|
2837
3023
|
parameters: Type.Object({
|
|
2838
3024
|
name: Type.Optional(Type.String({ description: "Project title" })),
|
|
2839
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." })),
|
|
2840
3027
|
goal: Type.Optional(Type.String({ description: "Main project goal / objective" })),
|
|
2841
3028
|
scope: Type.Optional(Type.Array(Type.String(), { description: "Replace in-scope items" })),
|
|
2842
3029
|
outOfScope: Type.Optional(Type.Array(Type.String(), { description: "Replace out-of-scope items" })),
|
|
@@ -2850,10 +3037,34 @@ export default function planPiExtension(pi) {
|
|
|
2850
3037
|
if (!st)
|
|
2851
3038
|
return { content: [{ type: "text", text: "No .planner/ found. Use plan_init first." }], details: {} };
|
|
2852
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
|
+
}
|
|
2853
3057
|
if (params.name !== undefined)
|
|
2854
3058
|
project.name = params.name.trim();
|
|
2855
3059
|
if (params.description !== undefined)
|
|
2856
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
|
+
}
|
|
2857
3068
|
if (params.goal !== undefined)
|
|
2858
3069
|
project.goal = params.goal.trim();
|
|
2859
3070
|
if (params.scope !== undefined)
|
|
@@ -2870,7 +3081,95 @@ export default function planPiExtension(pi) {
|
|
|
2870
3081
|
project.decisions = params.decisions.map((entry) => entry.trim()).filter(Boolean);
|
|
2871
3082
|
await st.saveProject(project);
|
|
2872
3083
|
await st.writeGenerated();
|
|
2873
|
-
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
|
+
};
|
|
2874
3173
|
},
|
|
2875
3174
|
});
|
|
2876
3175
|
pi.registerTool({
|
|
@@ -2899,6 +3198,11 @@ export default function planPiExtension(pi) {
|
|
|
2899
3198
|
description: Type.Optional(Type.String({ description: "Requirement description" })),
|
|
2900
3199
|
status: Type.Optional(Type.String({ description: "Initial status: planned|in-progress|done|blocked|canceled" })),
|
|
2901
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." })),
|
|
2902
3206
|
}),
|
|
2903
3207
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2904
3208
|
const st = await requirePlan(ctx);
|
|
@@ -2912,19 +3216,37 @@ export default function planPiExtension(pi) {
|
|
|
2912
3216
|
return { content: [{ type: "text", text: links.error }], details: {} };
|
|
2913
3217
|
const requirements = await st.loadRequirements();
|
|
2914
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
|
+
}
|
|
2915
3231
|
const requirement = {
|
|
2916
3232
|
id: createRequirementId(),
|
|
2917
3233
|
title,
|
|
2918
3234
|
description: params.description?.trim() ?? "",
|
|
2919
3235
|
status: params.status?.trim() ?? "planned",
|
|
2920
|
-
macroTasks
|
|
3236
|
+
macroTasks,
|
|
2921
3237
|
linkedPhaseIds: links.linkedPhaseIds,
|
|
2922
3238
|
sessionInfo: [],
|
|
2923
3239
|
createdAt: now,
|
|
2924
3240
|
updatedAt: now,
|
|
2925
3241
|
};
|
|
2926
3242
|
requirements.requirements.push(requirement);
|
|
2927
|
-
|
|
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
|
+
}
|
|
2928
3250
|
await st.writeGenerated();
|
|
2929
3251
|
return { content: [{ type: "text", text: `Requirement created: ${requirement.id}` }], details: requirement };
|
|
2930
3252
|
},
|
|
@@ -2939,6 +3261,12 @@ export default function planPiExtension(pi) {
|
|
|
2939
3261
|
description: Type.Optional(Type.String({ description: "Requirement description" })),
|
|
2940
3262
|
status: Type.Optional(Type.String({ description: "Status: planned|in-progress|done|blocked|canceled" })),
|
|
2941
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." })),
|
|
2942
3270
|
}),
|
|
2943
3271
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
2944
3272
|
const st = await requirePlan(ctx);
|
|
@@ -2964,7 +3292,22 @@ export default function planPiExtension(pi) {
|
|
|
2964
3292
|
return { content: [{ type: "text", text: links.error }], details: {} };
|
|
2965
3293
|
requirement.linkedPhaseIds = links.linkedPhaseIds;
|
|
2966
3294
|
}
|
|
2967
|
-
|
|
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;
|
|
2968
3311
|
await st.saveRequirements(requirements);
|
|
2969
3312
|
await st.writeGenerated();
|
|
2970
3313
|
return { content: [{ type: "text", text: `Requirement updated: ${requirement.id}` }], details: requirement };
|
|
@@ -2991,6 +3334,77 @@ export default function planPiExtension(pi) {
|
|
|
2991
3334
|
return { content: [{ type: "text", text: `Requirement deleted: ${params.requirementId}` }], details: { deleted: params.requirementId } };
|
|
2992
3335
|
},
|
|
2993
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
|
+
});
|
|
2994
3408
|
pi.registerTool({
|
|
2995
3409
|
name: "plan_get",
|
|
2996
3410
|
label: "Plan Get",
|
|
@@ -3089,9 +3503,11 @@ export default function planPiExtension(pi) {
|
|
|
3089
3503
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3090
3504
|
const list = await st.listHandoffs();
|
|
3091
3505
|
if (list.length === 0)
|
|
3092
|
-
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 } };
|
|
3093
|
-
const
|
|
3094
|
-
|
|
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 } };
|
|
3095
3511
|
},
|
|
3096
3512
|
});
|
|
3097
3513
|
pi.registerTool({
|
|
@@ -3182,17 +3598,22 @@ export default function planPiExtension(pi) {
|
|
|
3182
3598
|
pi.registerTool({
|
|
3183
3599
|
name: "handoff_list",
|
|
3184
3600
|
label: "Handoff List",
|
|
3185
|
-
description: "List
|
|
3186
|
-
parameters: Type.Object({
|
|
3187
|
-
|
|
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) {
|
|
3188
3607
|
const st = await requirePlan(ctx);
|
|
3189
3608
|
if (!st)
|
|
3190
3609
|
return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
|
|
3191
3610
|
const list = await st.listHandoffs();
|
|
3192
3611
|
if (list.length === 0)
|
|
3193
|
-
return { content: [{ type: "text", text: "No phase handoffs set." }], details: { count: 0 } };
|
|
3194
|
-
const
|
|
3195
|
-
|
|
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 } };
|
|
3196
3617
|
},
|
|
3197
3618
|
});
|
|
3198
3619
|
pi.registerTool({
|
|
@@ -3207,16 +3628,20 @@ export default function planPiExtension(pi) {
|
|
|
3207
3628
|
const r = await resolvePhaseForHandoff(st, params.phaseRef);
|
|
3208
3629
|
if (!r.ok)
|
|
3209
3630
|
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
3210
|
-
const
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
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
|
+
};
|
|
3214
3639
|
},
|
|
3215
3640
|
});
|
|
3216
3641
|
pi.registerTool({
|
|
3217
3642
|
name: "handoff_prepare",
|
|
3218
3643
|
label: "Handoff Prepare",
|
|
3219
|
-
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.",
|
|
3220
3645
|
parameters: Type.Object({ phaseRef: Type.String({ description: "Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title." }) }),
|
|
3221
3646
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3222
3647
|
const st = await requirePlan(ctx);
|
|
@@ -3227,8 +3652,10 @@ export default function planPiExtension(pi) {
|
|
|
3227
3652
|
return { content: [{ type: "text", text: `❌ ${r.error}` }], details: { error: r.error } };
|
|
3228
3653
|
try {
|
|
3229
3654
|
const audit = await st.preparePhaseHandoff(r.phase.id);
|
|
3655
|
+
const { handoff, ...auditDetails } = audit;
|
|
3656
|
+
const boundedExisting = boundedHandoffForTransport(handoff);
|
|
3230
3657
|
const missing = audit.missingCompletionTasks.map((task) => `- T${String(task.number).padStart(3, "0")} — ${task.title}`).join("\n") || "- None";
|
|
3231
|
-
const existing =
|
|
3658
|
+
const existing = boundedExisting.content.trim() || "(none)";
|
|
3232
3659
|
return {
|
|
3233
3660
|
content: [{ type: "text", text: [
|
|
3234
3661
|
`Handoff preparation audit for ${r.compositeRef}`,
|
|
@@ -3236,10 +3663,14 @@ export default function planPiExtension(pi) {
|
|
|
3236
3663
|
"Done tasks missing durable completion/verification evidence:",
|
|
3237
3664
|
missing,
|
|
3238
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
|
+
"",
|
|
3239
3670
|
"Existing active handoff (reconcile all still-relevant content):",
|
|
3240
3671
|
existing,
|
|
3241
3672
|
].join("\n") }],
|
|
3242
|
-
details: { phaseRef: r.compositeRef, ...
|
|
3673
|
+
details: { phaseRef: r.compositeRef, ...auditDetails, existingHandoffLength: boundedExisting.fullLength, existingHandoffTruncated: boundedExisting.truncated },
|
|
3243
3674
|
};
|
|
3244
3675
|
}
|
|
3245
3676
|
catch (error) {
|
|
@@ -3251,7 +3682,7 @@ export default function planPiExtension(pi) {
|
|
|
3251
3682
|
pi.registerTool({
|
|
3252
3683
|
name: "handoff_write",
|
|
3253
3684
|
label: "Handoff Write",
|
|
3254
|
-
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.",
|
|
3255
3686
|
parameters: Type.Object({
|
|
3256
3687
|
phaseRef: Type.String({ description: "Exact confirmed phase ref: P00x | P00x(F00x) | UUID | title." }),
|
|
3257
3688
|
title: Type.Optional(Type.String({ description: "Meaningful handoff title summarizing the work (becomes the H1 / first line in lists)." })),
|
|
@@ -3260,6 +3691,18 @@ export default function planPiExtension(pi) {
|
|
|
3260
3691
|
markdown_content: Type.Optional(Type.String({ description: "Handoff text (markdown). Preferred over content." })),
|
|
3261
3692
|
expectedHandoffUpdatedAt: Type.Optional(Type.String({ description: "Exact handoffUpdatedAt returned by handoff_prepare; use an empty string when no handoff exists." })),
|
|
3262
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
|
+
}))),
|
|
3263
3706
|
taskUpdates: Type.Optional(Type.Array(Type.Object({
|
|
3264
3707
|
taskRef: Type.String(),
|
|
3265
3708
|
completionSummary: Type.String(),
|
|
@@ -3328,6 +3771,8 @@ export default function planPiExtension(pi) {
|
|
|
3328
3771
|
content: body,
|
|
3329
3772
|
expectedHandoffUpdatedAt: params.expectedHandoffUpdatedAt,
|
|
3330
3773
|
reconciledExistingHandoff: params.reconciledExistingHandoff === true,
|
|
3774
|
+
...(params.completenessAudit ? { completenessAudit: params.completenessAudit } : {}),
|
|
3775
|
+
...(params.supportingDocuments ? { supportingDocuments: params.supportingDocuments } : {}),
|
|
3331
3776
|
contextSync: {
|
|
3332
3777
|
taskUpdates,
|
|
3333
3778
|
...(params.phaseUpdate ? { phaseUpdate: {
|
|
@@ -3341,9 +3786,20 @@ export default function planPiExtension(pi) {
|
|
|
3341
3786
|
},
|
|
3342
3787
|
});
|
|
3343
3788
|
await st.writeGenerated();
|
|
3344
|
-
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
|
+
};
|
|
3345
3799
|
}
|
|
3346
3800
|
catch (error) {
|
|
3801
|
+
if (error instanceof HandoffContractError)
|
|
3802
|
+
return handoffContractFailure(error);
|
|
3347
3803
|
const message = error instanceof Error ? error.message : String(error);
|
|
3348
3804
|
return { content: [{ type: "text", text: `❌ Handoff refresh denied: ${message}` }], details: { error: message } };
|
|
3349
3805
|
}
|
|
@@ -3398,7 +3854,7 @@ export default function planPiExtension(pi) {
|
|
|
3398
3854
|
pi.registerTool({
|
|
3399
3855
|
name: "feature_list",
|
|
3400
3856
|
label: "Feature List",
|
|
3401
|
-
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.",
|
|
3402
3858
|
parameters: Type.Object({ featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })) }),
|
|
3403
3859
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
3404
3860
|
const st = await requirePlan(ctx);
|
|
@@ -3411,7 +3867,7 @@ export default function planPiExtension(pi) {
|
|
|
3411
3867
|
const phases = await st.loadAllPhases();
|
|
3412
3868
|
const summary = features.map((f) => {
|
|
3413
3869
|
const fp = phases.filter((p) => p.featureId === f.id);
|
|
3414
|
-
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)`;
|
|
3415
3871
|
}).join("\n");
|
|
3416
3872
|
return { content: [{ type: "text", text: summary || "No features" }], details: {} };
|
|
3417
3873
|
},
|
|
@@ -3439,10 +3895,14 @@ export default function planPiExtension(pi) {
|
|
|
3439
3895
|
const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
|
|
3440
3896
|
const linkedRequirements = await st.linkedRequirementsForFeature(feature.id);
|
|
3441
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}` : "";
|
|
3442
3899
|
const requirementsBlock = linkedRequirements.length > 0
|
|
3443
3900
|
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
3444
3901
|
: "\n\nLinked requirements:\n- None linked to this feature.";
|
|
3445
|
-
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
|
+
};
|
|
3446
3906
|
},
|
|
3447
3907
|
});
|
|
3448
3908
|
pi.registerTool({
|
|
@@ -3505,6 +3965,7 @@ export default function planPiExtension(pi) {
|
|
|
3505
3965
|
parameters: Type.Object({
|
|
3506
3966
|
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
3507
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." })),
|
|
3508
3969
|
workDone: Type.Optional(Type.String({ description: "What is already implemented / decided" })),
|
|
3509
3970
|
workRemaining: Type.Optional(Type.String({ description: "What still needs to be done" })),
|
|
3510
3971
|
dependencies: Type.Optional(Type.Array(Type.String(), { description: "Cross-feature or external dependencies" })),
|
|
@@ -3518,16 +3979,41 @@ export default function planPiExtension(pi) {
|
|
|
3518
3979
|
if (!resolvedFeature.ok)
|
|
3519
3980
|
return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
|
|
3520
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
|
+
}
|
|
3521
4000
|
const updatedDoc = await st.updateFeatures((doc) => {
|
|
3522
4001
|
const feature = doc.features.find((f) => f.id === featureId);
|
|
3523
4002
|
if (!feature)
|
|
3524
4003
|
return doc;
|
|
3525
4004
|
if (params.description !== undefined)
|
|
3526
|
-
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
|
+
}
|
|
3527
4013
|
if (params.workDone !== undefined)
|
|
3528
|
-
feature.workDone = params.workDone;
|
|
4014
|
+
feature.workDone = params.workDone.trim();
|
|
3529
4015
|
if (params.workRemaining !== undefined)
|
|
3530
|
-
feature.workRemaining = params.workRemaining;
|
|
4016
|
+
feature.workRemaining = params.workRemaining.trim();
|
|
3531
4017
|
if (params.dependencies !== undefined)
|
|
3532
4018
|
feature.dependsOn = params.dependencies.map((item) => item.trim()).filter(Boolean);
|
|
3533
4019
|
feature.discussedAt = nowISO();
|
|
@@ -3540,7 +4026,7 @@ export default function planPiExtension(pi) {
|
|
|
3540
4026
|
if (!feature)
|
|
3541
4027
|
return { content: [{ type: "text", text: `Feature not found: ${params.featureId}` }], details: {} };
|
|
3542
4028
|
await st.writeGenerated();
|
|
3543
|
-
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 } };
|
|
3544
4030
|
},
|
|
3545
4031
|
});
|
|
3546
4032
|
pi.registerTool({
|
|
@@ -3551,6 +4037,7 @@ export default function planPiExtension(pi) {
|
|
|
3551
4037
|
featureId: Type.String({ description: "Feature ref: F00x, shortId, UUID, or name" }),
|
|
3552
4038
|
name: Type.Optional(Type.String({ description: "New name" })),
|
|
3553
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." })),
|
|
3554
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." })),
|
|
3555
4042
|
startDate: Type.Optional(Type.String({ description: "Start date (YYYY-MM-DD)" })),
|
|
3556
4043
|
endDate: Type.Optional(Type.String({ description: "End date (YYYY-MM-DD)" })),
|
|
@@ -3581,6 +4068,7 @@ export default function planPiExtension(pi) {
|
|
|
3581
4068
|
const mutableFields = [
|
|
3582
4069
|
"name",
|
|
3583
4070
|
"description",
|
|
4071
|
+
"descriptionRef",
|
|
3584
4072
|
"startDate",
|
|
3585
4073
|
"endDate",
|
|
3586
4074
|
"workDone",
|
|
@@ -3590,13 +4078,20 @@ export default function planPiExtension(pi) {
|
|
|
3590
4078
|
];
|
|
3591
4079
|
const receivedFields = mutableFields.filter((field) => params[field] !== undefined);
|
|
3592
4080
|
if (receivedFields.length === 0) {
|
|
3593
|
-
return {
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
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
|
+
});
|
|
3600
4095
|
}
|
|
3601
4096
|
let foundDoc;
|
|
3602
4097
|
try {
|
|
@@ -3605,17 +4100,24 @@ export default function planPiExtension(pi) {
|
|
|
3605
4100
|
if (!feature)
|
|
3606
4101
|
return doc; // not found: no-op, handled below
|
|
3607
4102
|
if (params.name !== undefined)
|
|
3608
|
-
feature.name = params.name;
|
|
4103
|
+
feature.name = params.name.trim();
|
|
3609
4104
|
if (params.description !== undefined)
|
|
3610
|
-
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
|
+
}
|
|
3611
4113
|
if (params.workDone !== undefined)
|
|
3612
|
-
feature.workDone = params.workDone;
|
|
4114
|
+
feature.workDone = params.workDone.trim();
|
|
3613
4115
|
if (params.workRemaining !== undefined)
|
|
3614
|
-
feature.workRemaining = params.workRemaining;
|
|
4116
|
+
feature.workRemaining = params.workRemaining.trim();
|
|
3615
4117
|
if (params.startDate !== undefined)
|
|
3616
|
-
feature.startDate = params.startDate;
|
|
4118
|
+
feature.startDate = params.startDate.trim();
|
|
3617
4119
|
if (params.endDate !== undefined)
|
|
3618
|
-
feature.endDate = params.endDate;
|
|
4120
|
+
feature.endDate = params.endDate.trim();
|
|
3619
4121
|
if (params.priority !== undefined)
|
|
3620
4122
|
feature.priority = params.priority;
|
|
3621
4123
|
if (params.acceptedDecisions !== undefined)
|
|
@@ -3681,7 +4183,7 @@ export default function planPiExtension(pi) {
|
|
|
3681
4183
|
pi.registerTool({
|
|
3682
4184
|
name: "phase_list",
|
|
3683
4185
|
label: "Phase List",
|
|
3684
|
-
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.",
|
|
3685
4187
|
parameters: Type.Object({
|
|
3686
4188
|
featureRef: Type.Optional(Type.String({ description: "Optional: filter to one feature (F00x/shortId/UUID/name)" })),
|
|
3687
4189
|
status: Type.Optional(Type.String({ description: "Optional: filter by status name" })),
|
|
@@ -3704,7 +4206,7 @@ export default function planPiExtension(pi) {
|
|
|
3704
4206
|
const summary = phases.map((p) => {
|
|
3705
4207
|
const fNum = featureNumberOfPhase(p, features);
|
|
3706
4208
|
const fTag = fNum !== undefined ? ` [F${pad(fNum)}]` : "";
|
|
3707
|
-
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}`;
|
|
3708
4210
|
}).join("\n");
|
|
3709
4211
|
return { content: [{ type: "text", text: summary || "No phases" }], details: {} };
|
|
3710
4212
|
},
|
|
@@ -3730,10 +4232,14 @@ export default function planPiExtension(pi) {
|
|
|
3730
4232
|
const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
|
|
3731
4233
|
const reqCount = linkedRequirements.length;
|
|
3732
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}` : "";
|
|
3733
4236
|
const requirementsBlock = reqCount > 0
|
|
3734
4237
|
? `\n\nLinked requirements:\n${linkedRequirements.map((requirement) => `- ${requirement.title} (${requirement.status})`).join("\n")}`
|
|
3735
4238
|
: "";
|
|
3736
|
-
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
|
+
};
|
|
3737
4243
|
},
|
|
3738
4244
|
});
|
|
3739
4245
|
pi.registerTool({
|
|
@@ -3784,7 +4290,7 @@ export default function planPiExtension(pi) {
|
|
|
3784
4290
|
goals: [], nonGoals: [], dependencies: [], dependsOn: [], risks: [],
|
|
3785
4291
|
openQuestions: [], decisions: [], acceptedDecisions: [], completionCriteria: [], taskIds: [], tasks: [],
|
|
3786
4292
|
createdAt: now, updatedAt: now,
|
|
3787
|
-
handoff: "", handoffUpdatedAt: "",
|
|
4293
|
+
handoff: "", handoffUpdatedAt: "", handoffAudit: null,
|
|
3788
4294
|
handoffReadAt: "",
|
|
3789
4295
|
handoffHistory: [],
|
|
3790
4296
|
statusLog: [],
|
|
@@ -3818,6 +4324,7 @@ export default function planPiExtension(pi) {
|
|
|
3818
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." })),
|
|
3819
4325
|
summary: Type.Optional(Type.String({ description: "New summary" })),
|
|
3820
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." })),
|
|
3821
4328
|
featureId: Type.Optional(Type.String({ description: "Link/unlink phase to a feature. Use empty string to unlink." })),
|
|
3822
4329
|
priority: Type.Optional(Type.Number({ description: "Display order within the feature (lower = higher)" })),
|
|
3823
4330
|
goals: Type.Optional(Type.Array(Type.String(), { description: "Replace goals list" })),
|
|
@@ -3855,34 +4362,59 @@ export default function planPiExtension(pi) {
|
|
|
3855
4362
|
if (params.status !== undefined) {
|
|
3856
4363
|
return derivedStatusReadOnlyResult("phase", formatPhaseRef(resolvedPhase.number, featureNumberOfPhase(resolvedPhase, features)), String(params.status), resolvedPhase.status);
|
|
3857
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
|
+
}
|
|
3858
4383
|
const prevFeatureId = phase.featureId;
|
|
3859
4384
|
let nextFeatureIdForUpdate = prevFeatureId;
|
|
3860
4385
|
if (params.title !== undefined) {
|
|
3861
|
-
phase.title = params.title;
|
|
4386
|
+
phase.title = params.title.trim();
|
|
3862
4387
|
phase.slug = normalizeSlug(params.title);
|
|
3863
4388
|
}
|
|
3864
4389
|
if (params.summary !== undefined)
|
|
3865
|
-
phase.summary = params.summary;
|
|
4390
|
+
phase.summary = params.summary.trim();
|
|
3866
4391
|
if (params.description !== undefined)
|
|
3867
|
-
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
|
+
}
|
|
3868
4400
|
if (params.priority !== undefined)
|
|
3869
4401
|
phase.priority = params.priority;
|
|
3870
4402
|
if (params.goals !== undefined)
|
|
3871
|
-
phase.goals = params.goals;
|
|
4403
|
+
phase.goals = params.goals.map((item) => item.trim()).filter(Boolean);
|
|
3872
4404
|
if (params.nonGoals !== undefined)
|
|
3873
|
-
phase.nonGoals = params.nonGoals;
|
|
4405
|
+
phase.nonGoals = params.nonGoals.map((item) => item.trim()).filter(Boolean);
|
|
3874
4406
|
if (params.dependencies !== undefined)
|
|
3875
|
-
phase.dependencies = params.dependencies;
|
|
4407
|
+
phase.dependencies = params.dependencies.map((item) => item.trim()).filter(Boolean);
|
|
3876
4408
|
if (params.risks !== undefined)
|
|
3877
|
-
phase.risks = params.risks;
|
|
4409
|
+
phase.risks = params.risks.map((item) => item.trim()).filter(Boolean);
|
|
3878
4410
|
if (params.openQuestions !== undefined)
|
|
3879
|
-
phase.openQuestions = params.openQuestions;
|
|
4411
|
+
phase.openQuestions = params.openQuestions.map((item) => item.trim()).filter(Boolean);
|
|
3880
4412
|
if (params.decisions !== undefined)
|
|
3881
|
-
phase.decisions = params.decisions;
|
|
4413
|
+
phase.decisions = params.decisions.map((item) => item.trim()).filter(Boolean);
|
|
3882
4414
|
if (params.acceptedDecisions !== undefined)
|
|
3883
4415
|
phase.acceptedDecisions = params.acceptedDecisions;
|
|
3884
4416
|
if (params.completionCriteria !== undefined)
|
|
3885
|
-
phase.completionCriteria = params.completionCriteria;
|
|
4417
|
+
phase.completionCriteria = params.completionCriteria.map((item) => item.trim()).filter(Boolean);
|
|
3886
4418
|
if (params.featureId !== undefined) {
|
|
3887
4419
|
if (params.featureId === "") {
|
|
3888
4420
|
nextFeatureIdForUpdate = undefined;
|
|
@@ -3910,7 +4442,7 @@ export default function planPiExtension(pi) {
|
|
|
3910
4442
|
await st.savePhase(phase);
|
|
3911
4443
|
await st.writeGenerated();
|
|
3912
4444
|
const phaseUpdateFeatures = (await st.loadFeatures()).features;
|
|
3913
|
-
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 } };
|
|
3914
4446
|
},
|
|
3915
4447
|
});
|
|
3916
4448
|
pi.registerTool({
|
|
@@ -4026,7 +4558,7 @@ export default function planPiExtension(pi) {
|
|
|
4026
4558
|
pi.registerTool({
|
|
4027
4559
|
name: "task_list",
|
|
4028
4560
|
label: "Task List",
|
|
4029
|
-
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.",
|
|
4030
4562
|
parameters: Type.Object({
|
|
4031
4563
|
featureRef: Type.Optional(Type.String({})),
|
|
4032
4564
|
phaseRef: Type.Optional(Type.String({})),
|
|
@@ -4057,7 +4589,7 @@ export default function planPiExtension(pi) {
|
|
|
4057
4589
|
for (const task of phase.tasks) {
|
|
4058
4590
|
if (params.status && task.status !== params.status)
|
|
4059
4591
|
continue;
|
|
4060
|
-
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}` : ""})`);
|
|
4061
4593
|
}
|
|
4062
4594
|
}
|
|
4063
4595
|
return { content: [{ type: "text", text: out.join("\n") || "No tasks" }], details: {} };
|
|
@@ -4096,6 +4628,8 @@ export default function planPiExtension(pi) {
|
|
|
4096
4628
|
const sections = [summary];
|
|
4097
4629
|
if (found.task.description?.trim())
|
|
4098
4630
|
sections.push(found.task.description.trim());
|
|
4631
|
+
if (found.task.descriptionRef)
|
|
4632
|
+
sections.push(`Description reference:\n- ${found.task.descriptionRef}`);
|
|
4099
4633
|
if (snapshot || pendingDeviation) {
|
|
4100
4634
|
sections.push([
|
|
4101
4635
|
pendingDeviation
|
|
@@ -4109,7 +4643,7 @@ export default function planPiExtension(pi) {
|
|
|
4109
4643
|
}
|
|
4110
4644
|
if (log)
|
|
4111
4645
|
sections.push(`Status log:\n${log}`);
|
|
4112
|
-
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 ?? "" } } };
|
|
4113
4647
|
},
|
|
4114
4648
|
});
|
|
4115
4649
|
pi.registerTool({
|
|
@@ -4211,6 +4745,7 @@ export default function planPiExtension(pi) {
|
|
|
4211
4745
|
title: Type.Optional(Type.String({ description: "New title" })),
|
|
4212
4746
|
status: Type.Optional(Type.String({ description: "New status: planned|in-progress|done|blocked|canceled|rejected|deferred|waiting" })),
|
|
4213
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." })),
|
|
4214
4749
|
notes: Type.Optional(Type.String({ description: "New implementation notes" })),
|
|
4215
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." })),
|
|
4216
4751
|
decisions: Type.Optional(Type.Array(Type.String(), { description: "Replace decisions list" })),
|
|
@@ -4262,6 +4797,24 @@ export default function planPiExtension(pi) {
|
|
|
4262
4797
|
};
|
|
4263
4798
|
}
|
|
4264
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
|
+
}
|
|
4265
4818
|
try {
|
|
4266
4819
|
const updatedPhase = await st.updatePhase(hostPhase.id, (phase) => {
|
|
4267
4820
|
const task = phase.tasks.find((t) => t.id === found.task.id);
|
|
@@ -4272,11 +4825,18 @@ export default function planPiExtension(pi) {
|
|
|
4272
4825
|
if (params.priority !== undefined)
|
|
4273
4826
|
task.priority = params.priority;
|
|
4274
4827
|
if (params.description !== undefined)
|
|
4275
|
-
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
|
+
}
|
|
4276
4836
|
if (params.notes !== undefined)
|
|
4277
|
-
task.notes = params.notes;
|
|
4837
|
+
task.notes = params.notes.trim();
|
|
4278
4838
|
if (params.decisions !== undefined)
|
|
4279
|
-
task.decisions = params.decisions;
|
|
4839
|
+
task.decisions = params.decisions.map((item) => item.trim()).filter(Boolean);
|
|
4280
4840
|
if (params.acceptedDecisions !== undefined)
|
|
4281
4841
|
task.acceptedDecisions = params.acceptedDecisions;
|
|
4282
4842
|
if (params.checklist !== undefined) {
|
|
@@ -4314,7 +4874,7 @@ export default function planPiExtension(pi) {
|
|
|
4314
4874
|
return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
|
|
4315
4875
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
4316
4876
|
await st.writeGenerated();
|
|
4317
|
-
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 } };
|
|
4318
4878
|
},
|
|
4319
4879
|
});
|
|
4320
4880
|
pi.registerTool({
|
|
@@ -4463,7 +5023,7 @@ export default function planPiExtension(pi) {
|
|
|
4463
5023
|
pi.registerTool({
|
|
4464
5024
|
name: "task_switch",
|
|
4465
5025
|
label: "Task Switch",
|
|
4466
|
-
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. Atomically snapshot/pause the source, start the target, and push a durable LIFO return target.",
|
|
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.",
|
|
4467
5027
|
parameters: Type.Object({
|
|
4468
5028
|
from_task: Type.String({ description: "Currently in-progress task ref" }),
|
|
4469
5029
|
to_task: Type.String({ description: "Temporary task ref to start" }),
|
|
@@ -4509,11 +5069,20 @@ export default function planPiExtension(pi) {
|
|
|
4509
5069
|
};
|
|
4510
5070
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
4511
5071
|
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
5072
|
+
const projectGuidelinesReadState = projectGuidelinesReadStateForSession(project, plannerSessionId);
|
|
5073
|
+
const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
|
|
4512
5074
|
const targetPhaseRef = formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features));
|
|
4513
5075
|
const targetTaskRef = `${targetPhaseRef}/T${String(target.task.number).padStart(3, "0")}`;
|
|
4514
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
|
+
}
|
|
4515
5084
|
if (!contextEligibility.eligible) {
|
|
4516
|
-
const nextActions = piContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, !requirementsReady, `Retry task_switch to ${targetTaskRef}`);
|
|
5085
|
+
const nextActions = piContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, false, !requirementsReady, `Retry task_switch to ${targetTaskRef}`);
|
|
4517
5086
|
return { content: [{ type: "text", text: `Task switch denied: context reads required. ${contextEligibility.reason}` }], details: { ...contextEligibility, nextActions } };
|
|
4518
5087
|
}
|
|
4519
5088
|
if (!requirementsReady) {
|
|
@@ -4628,7 +5197,7 @@ export default function planPiExtension(pi) {
|
|
|
4628
5197
|
pi.registerTool({
|
|
4629
5198
|
name: "task_start",
|
|
4630
5199
|
label: "Task Start",
|
|
4631
|
-
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, in order, and
|
|
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.",
|
|
4632
5201
|
parameters: Type.Object({
|
|
4633
5202
|
taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
|
|
4634
5203
|
}),
|
|
@@ -4646,7 +5215,8 @@ export default function planPiExtension(pi) {
|
|
|
4646
5215
|
const feature = found.phase.featureId ? features.find((candidate) => candidate.id === found.phase.featureId) : undefined;
|
|
4647
5216
|
const featureRef = feature ? formatFeatureRef(feature.number) : "";
|
|
4648
5217
|
if (task.status === "done")
|
|
4649
|
-
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();
|
|
4650
5220
|
const linkedRequirements = [
|
|
4651
5221
|
...(await st.linkedRequirementsForPhase(found.phase.id)),
|
|
4652
5222
|
...(found.phase.featureId ? await st.linkedRequirementsForFeature(found.phase.featureId) : []),
|
|
@@ -4665,8 +5235,13 @@ export default function planPiExtension(pi) {
|
|
|
4665
5235
|
};
|
|
4666
5236
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
4667
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
|
+
}
|
|
4668
5243
|
if (!contextEligibility.eligible) {
|
|
4669
|
-
return taskStartFailure(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, piContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, !requirementsReady, `Retry task_start ${taskRef}`), { taskId: task.id }), { contextEligibility });
|
|
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 });
|
|
4670
5245
|
}
|
|
4671
5246
|
if (!requirementsReady) {
|
|
4672
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 }));
|
|
@@ -4684,7 +5259,7 @@ export default function planPiExtension(pi) {
|
|
|
4684
5259
|
const outcome = taskStartSucceeded(task.id, true);
|
|
4685
5260
|
return { content: [{ type: "text", text: `✅ Task already started: ${taskRef} — ${task.title} (in-progress)\nstarted: true` }], details: { ...task, ...outcome, task } };
|
|
4686
5261
|
}
|
|
4687
|
-
const [
|
|
5262
|
+
const [phases, focus] = await Promise.all([st.loadAllPhases(), st.loadResume()]);
|
|
4688
5263
|
const eligibility = checkExplicitTaskStart(features, phases, task.id, project.workDeviations);
|
|
4689
5264
|
if (!eligibility.eligible)
|
|
4690
5265
|
return taskStartFailure(taskStartDenied("START_NOT_ALLOWED", eligibility.reason, ["Resolve the reported task readiness condition, then retry task_start."], { taskId: task.id }), { eligibility });
|
|
@@ -5036,10 +5611,24 @@ export default function planPiExtension(pi) {
|
|
|
5036
5611
|
const st = await requirePlan(ctx);
|
|
5037
5612
|
if (!st)
|
|
5038
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
|
+
}
|
|
5039
5621
|
enablePlannerSession();
|
|
5040
5622
|
if (!server) {
|
|
5041
5623
|
await startServer(ctx, undefined, "lan").catch(() => { });
|
|
5042
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;
|
|
5043
5632
|
const srv = server;
|
|
5044
5633
|
let recap = "";
|
|
5045
5634
|
try {
|
|
@@ -5048,7 +5637,9 @@ export default function planPiExtension(pi) {
|
|
|
5048
5637
|
catch (e) {
|
|
5049
5638
|
recap = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
|
|
5050
5639
|
}
|
|
5051
|
-
|
|
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 } } };
|
|
5052
5643
|
},
|
|
5053
5644
|
});
|
|
5054
5645
|
pi.registerTool({
|
|
@@ -5109,6 +5700,7 @@ export default function planPiExtension(pi) {
|
|
|
5109
5700
|
const recentActivity = activity.entries.slice(-3).reverse();
|
|
5110
5701
|
const handoffs = await st.listHandoffs();
|
|
5111
5702
|
const extensionRules = await st.extensionRules();
|
|
5703
|
+
const plannerSkill = await st.syncPlannerSkill();
|
|
5112
5704
|
const featureById = new Map(plan.features.features.map((feature) => [feature.id, feature]));
|
|
5113
5705
|
const phaseById = new Map(plan.phases.map((phase) => [phase.id, phase]));
|
|
5114
5706
|
const orderedPhases = [
|
|
@@ -5223,11 +5815,11 @@ export default function planPiExtension(pi) {
|
|
|
5223
5815
|
"",
|
|
5224
5816
|
"Operational rules:",
|
|
5225
5817
|
"- Handoffs are context, not locks. Read the relevant handoff with handoff show <ref> before resuming.",
|
|
5226
|
-
"- BEFORE work: call task_start or task_switch first so valid sessionInfo attestations are reused. If denied, perform only the missing/stale
|
|
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.",
|
|
5227
5819
|
"- Record every new decision or user-agreed modification in both the relevant feature and phase before treating the discussion as complete.",
|
|
5228
5820
|
"- Use task_update with motivation for blocked/canceled/rejected/deferred/waiting/planned(from non-planned).",
|
|
5229
5821
|
"- Planner ops (status/handoff/planner metadata) are NOT code edits; they are always allowed.",
|
|
5230
|
-
"- 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).",
|
|
5231
5823
|
"- If edit/write guard blocks you, start the right task or use an explicit bypass.",
|
|
5232
5824
|
...(extensionRules.length > 0
|
|
5233
5825
|
? [
|
|
@@ -5237,12 +5829,20 @@ export default function planPiExtension(pi) {
|
|
|
5237
5829
|
]
|
|
5238
5830
|
: []),
|
|
5239
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
|
+
"",
|
|
5240
5837
|
`Goal: ${project.goal || "(not set)"}`,
|
|
5241
5838
|
project.description ? `Description: ${project.description}` : "",
|
|
5242
5839
|
`Stack: ${[...project.technologies, ...project.tools].join(", ") || "(not set)"}`,
|
|
5243
5840
|
`Codebase: ${pkgSummary} | dirs=${profile?.directories.join(",") || "none"} | scanned=${profile?.scannedAt ?? "never"}`,
|
|
5244
5841
|
`Ambient: ${ambientSummary}`,
|
|
5245
5842
|
"",
|
|
5843
|
+
"Project Guidelines (mandatory when present):",
|
|
5844
|
+
project.projectGuidelines.content.trim() || "- (none)",
|
|
5845
|
+
"",
|
|
5246
5846
|
"Global rules:",
|
|
5247
5847
|
project.globalRules.length ? project.globalRules.map((rule) => `- ${rule}`).join("\n") : "- (none)",
|
|
5248
5848
|
"",
|