@agent-plan/core 0.3.0 → 0.3.2
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/guard-decision.d.ts +53 -43
- package/dist/guard-decision.d.ts.map +1 -1
- package/dist/guard-decision.js +134 -68
- package/dist/guard-state.d.ts +14 -0
- package/dist/guard-state.d.ts.map +1 -0
- package/dist/guard-state.js +35 -0
- package/dist/handoff-reply.d.ts +5 -0
- package/dist/handoff-reply.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/load-reply.d.ts +63 -0
- package/dist/load-reply.d.ts.map +1 -0
- package/dist/load-reply.js +64 -0
- package/dist/on-hold.d.ts +59 -0
- package/dist/on-hold.d.ts.map +1 -0
- package/dist/on-hold.js +150 -0
- package/dist/package-version.d.ts +13 -0
- package/dist/package-version.d.ts.map +1 -1
- package/dist/package-version.js +32 -0
- package/dist/project-context.d.ts +6 -4
- package/dist/project-context.d.ts.map +1 -1
- package/dist/project-context.js +2 -3
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +6 -0
- package/dist/recommend-reply.d.ts.map +1 -1
- package/dist/recommend-reply.js +10 -1
- package/dist/task-context.d.ts.map +1 -1
- package/dist/task-context.js +23 -13
- package/dist/task-move.d.ts +82 -0
- package/dist/task-move.d.ts.map +1 -0
- package/dist/task-move.js +169 -0
- package/dist/task-selection.d.ts +4 -1
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +22 -2
- package/package.json +1 -1
- package/planner-skill.md +3 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createStatusLogEntryId, featureNumberOfPhase, formatPhaseRef, formatThreeDigitNumber } from "./naming.js";
|
|
2
|
+
/** Phase statuses a task cannot be moved into. Same terminal set as tasks
|
|
3
|
+
* (done/canceled/rejected) — a terminal phase is meant to stay closed. */
|
|
4
|
+
const TERMINAL_PHASE_STATUSES = new Set(["done", "canceled", "rejected"]);
|
|
5
|
+
function taskRef(task, phase, features) {
|
|
6
|
+
return `${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}/T${formatThreeDigitNumber(task.number)}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Move `taskIds` (task UUIDs, already resolved by the caller) into
|
|
10
|
+
* `targetPhaseId` (a phase UUID). Validates target-exists, source != target
|
|
11
|
+
* per task, and refuses a terminal target phase — a terminal phase is
|
|
12
|
+
* closed, and reopening it by dropping new work in is not this operation's
|
|
13
|
+
* job (accepted decision on P108(F005)/T443: no override flag; unlink or
|
|
14
|
+
* reopen the phase first). An in-progress task MAY move; the reply says so.
|
|
15
|
+
*/
|
|
16
|
+
export async function moveTasks(store, taskIds, targetPhaseId, timestamp = new Date().toISOString()) {
|
|
17
|
+
const uniqueTaskIds = [...new Set(taskIds)];
|
|
18
|
+
if (uniqueTaskIds.length === 0) {
|
|
19
|
+
return { ok: false, errorCode: "NO_TASKS_GIVEN", error: "No task ids given to move." };
|
|
20
|
+
}
|
|
21
|
+
return store.runBatch(async () => {
|
|
22
|
+
const [phases, featuresDoc] = await Promise.all([store.loadAllPhases(), store.loadFeatures()]);
|
|
23
|
+
const features = featuresDoc.features;
|
|
24
|
+
const targetPhase = phases.find((phase) => phase.id === targetPhaseId);
|
|
25
|
+
if (!targetPhase) {
|
|
26
|
+
return { ok: false, errorCode: "TARGET_PHASE_NOT_FOUND", error: `Target phase not found: ${targetPhaseId}` };
|
|
27
|
+
}
|
|
28
|
+
if (TERMINAL_PHASE_STATUSES.has(targetPhase.status)) {
|
|
29
|
+
const targetRef = formatPhaseRef(targetPhase.number, featureNumberOfPhase(targetPhase, features));
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
errorCode: "TARGET_PHASE_TERMINAL",
|
|
33
|
+
error: `Target phase ${targetRef} is ${targetPhase.status}; cannot move tasks into a terminal phase.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const targetPhaseRef = formatPhaseRef(targetPhase.number, featureNumberOfPhase(targetPhase, features));
|
|
37
|
+
const allTasks = phases.flatMap((phase) => phase.tasks.map((task) => ({ phase, task })));
|
|
38
|
+
const located = [];
|
|
39
|
+
for (const taskId of uniqueTaskIds) {
|
|
40
|
+
const hit = allTasks.find((entry) => entry.task.id === taskId);
|
|
41
|
+
if (!hit)
|
|
42
|
+
return { ok: false, errorCode: "TASK_NOT_FOUND", error: `Task not found: ${taskId}` };
|
|
43
|
+
if (hit.phase.id === targetPhaseId) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
errorCode: "SOURCE_EQUALS_TARGET",
|
|
47
|
+
error: `Task ${taskRef(hit.task, hit.phase, features)} is already in ${targetPhaseRef}.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
located.push(hit);
|
|
51
|
+
}
|
|
52
|
+
// Group by source phase so a multi-source move removes each phase's
|
|
53
|
+
// tasks in one write, and rollback restores exactly what was removed.
|
|
54
|
+
const bySourcePhase = new Map();
|
|
55
|
+
for (const entry of located) {
|
|
56
|
+
const bucket = bySourcePhase.get(entry.phase.id) ?? { phase: entry.phase, tasks: [] };
|
|
57
|
+
bucket.tasks.push(entry.task);
|
|
58
|
+
bySourcePhase.set(entry.phase.id, bucket);
|
|
59
|
+
}
|
|
60
|
+
const writtenSourcePhaseIds = [];
|
|
61
|
+
const summaries = [];
|
|
62
|
+
const sourcePhaseRefsWithHandoff = [...bySourcePhase.values()]
|
|
63
|
+
.filter((bucket) => bucket.phase.handoff !== "")
|
|
64
|
+
.map((bucket) => formatPhaseRef(bucket.phase.number, featureNumberOfPhase(bucket.phase, features)));
|
|
65
|
+
try {
|
|
66
|
+
for (const [sourcePhaseId, bucket] of bySourcePhase) {
|
|
67
|
+
const idsToRemove = new Set(bucket.tasks.map((task) => task.id));
|
|
68
|
+
await store.updatePhase(sourcePhaseId, (phase) => {
|
|
69
|
+
phase.tasks = phase.tasks.filter((task) => !idsToRemove.has(task.id));
|
|
70
|
+
return phase;
|
|
71
|
+
});
|
|
72
|
+
writtenSourcePhaseIds.push(sourcePhaseId);
|
|
73
|
+
}
|
|
74
|
+
const movedTasks = located.map(({ phase: sourcePhase, task }) => {
|
|
75
|
+
const oldRef = taskRef(task, sourcePhase, features);
|
|
76
|
+
const newRef = `${targetPhaseRef}/T${formatThreeDigitNumber(task.number)}`;
|
|
77
|
+
summaries.push({ taskId: task.id, shortId: task.shortId, title: task.title, status: task.status, oldRef, newRef });
|
|
78
|
+
return {
|
|
79
|
+
...task,
|
|
80
|
+
phaseId: targetPhaseId,
|
|
81
|
+
statusLog: [...task.statusLog, {
|
|
82
|
+
id: createStatusLogEntryId(),
|
|
83
|
+
date: timestamp,
|
|
84
|
+
fromStatus: task.status,
|
|
85
|
+
toStatus: task.status,
|
|
86
|
+
title: `moved ${oldRef} → ${newRef}`,
|
|
87
|
+
description: `Moved from ${oldRef} to ${newRef}; status unchanged.`,
|
|
88
|
+
}],
|
|
89
|
+
updatedAt: timestamp,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
await store.updatePhase(targetPhaseId, (phase) => {
|
|
93
|
+
phase.tasks = [...phase.tasks, ...movedTasks];
|
|
94
|
+
return phase;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// Roll back every source phase already written. Add the ORIGINAL task
|
|
99
|
+
// objects back rather than restoring a whole array snapshot, so a
|
|
100
|
+
// concurrent unrelated write to the same file (another task added or
|
|
101
|
+
// edited while this move was in flight) is not clobbered.
|
|
102
|
+
for (const sourcePhaseId of writtenSourcePhaseIds) {
|
|
103
|
+
const bucket = bySourcePhase.get(sourcePhaseId);
|
|
104
|
+
if (!bucket)
|
|
105
|
+
continue;
|
|
106
|
+
await store.updatePhase(sourcePhaseId, (phase) => {
|
|
107
|
+
phase.tasks = [...phase.tasks, ...bucket.tasks];
|
|
108
|
+
return phase;
|
|
109
|
+
}).catch(() => { });
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
// Roll up derived status the same way every other task mutation does:
|
|
114
|
+
// a phase whose task count just changed may have crossed into or out of
|
|
115
|
+
// a terminal status (handoff archiving, phase/feature statusLog, resume
|
|
116
|
+
// refresh all live in syncTaskStatusRollup — callers must not reimplement
|
|
117
|
+
// them). Every touched phase gets it, not just the target.
|
|
118
|
+
for (const sourcePhaseId of writtenSourcePhaseIds) {
|
|
119
|
+
await store.syncTaskStatusRollup(sourcePhaseId);
|
|
120
|
+
}
|
|
121
|
+
await store.syncTaskStatusRollup(targetPhaseId);
|
|
122
|
+
await store.writeGenerated();
|
|
123
|
+
// Prove it happened: re-read from disk rather than trusting the
|
|
124
|
+
// in-memory write — same discipline as deleteFeatureCascade.
|
|
125
|
+
const persistedTarget = await store.loadPhase(targetPhaseId);
|
|
126
|
+
const persistedIds = new Set(persistedTarget.tasks.map((task) => task.id));
|
|
127
|
+
if (summaries.some((move) => !persistedIds.has(move.taskId))) {
|
|
128
|
+
return { ok: false, errorCode: "TASK_NOT_FOUND", error: `Task move did not persist to ${targetPhaseRef}.` };
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, result: { targetPhaseRef, moves: summaries, sourcePhaseRefsWithHandoff } };
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Cap on how many moved tasks a reply names individually. Moving one task
|
|
134
|
+
* or a handful (the documented case) always prints every ref; a very large
|
|
135
|
+
* batch degrades to a count rather than growing the reply without bound. */
|
|
136
|
+
const MAX_TASK_MOVE_REPLY_ITEMS = 20;
|
|
137
|
+
/**
|
|
138
|
+
* The one reply planner-task-move / task_move return. Deliberately NOT
|
|
139
|
+
* built with buildMutationReply: that helper echoes one entity's changed
|
|
140
|
+
* field values, and the only value that changed here — phaseId — is not
|
|
141
|
+
* something a caller refers to directly. What a caller needs per task is
|
|
142
|
+
* its old ref, its new ref, and enough identity (id/shortId/title/status)
|
|
143
|
+
* to confirm it moved — never the description, checklist or statusLog it
|
|
144
|
+
* already has and did not ask to resend.
|
|
145
|
+
*/
|
|
146
|
+
export function buildTaskMoveReply(result, readBackCommand) {
|
|
147
|
+
const shown = result.moves.slice(0, MAX_TASK_MOVE_REPLY_ITEMS);
|
|
148
|
+
const omitted = result.moves.length - shown.length;
|
|
149
|
+
const lines = shown.map((move) => `${move.oldRef} → ${move.newRef} — ${move.title} [${move.status}]${move.shortId ? ` · ${move.shortId}` : ""}`);
|
|
150
|
+
const handoffNotice = result.sourcePhaseRefsWithHandoff.length > 0
|
|
151
|
+
? [`⚠️ Pending handoff on ${result.sourcePhaseRefsWithHandoff.join(", ")} may reference the moved task(s) by ref; it is prose and was not rewritten — check it.`]
|
|
152
|
+
: [];
|
|
153
|
+
const text = [
|
|
154
|
+
`✅ Moved ${result.moves.length} task(s) to ${result.targetPhaseRef}:`,
|
|
155
|
+
...lines,
|
|
156
|
+
...(omitted > 0 ? [`(+${omitted} more)`] : []),
|
|
157
|
+
...handoffNotice,
|
|
158
|
+
`Read back: ${readBackCommand}`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
const structured = {
|
|
161
|
+
moved: true,
|
|
162
|
+
targetPhaseRef: result.targetPhaseRef,
|
|
163
|
+
moves: shown.map((move) => ({ id: move.taskId, shortId: move.shortId, title: move.title, status: move.status, oldRef: move.oldRef, newRef: move.newRef })),
|
|
164
|
+
...(omitted > 0 ? { omittedMoves: omitted } : {}),
|
|
165
|
+
...(result.sourcePhaseRefsWithHandoff.length > 0 ? { sourcePhaseRefsWithHandoff: result.sourcePhaseRefsWithHandoff } : {}),
|
|
166
|
+
readBack: readBackCommand,
|
|
167
|
+
};
|
|
168
|
+
return { text, structured };
|
|
169
|
+
}
|
package/dist/task-selection.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Feature, Phase, Task, WorkDeviation } from "./schema.js";
|
|
2
|
+
import { type OnHoldTask } from "./on-hold.js";
|
|
2
3
|
export type RecommendationClaimKind = "recommendation" | "advisory" | "conflict" | "insufficient";
|
|
3
|
-
export type RecommendationClaimSource = "priority" | "active-task" | "resume-deviation" | "dependency-ready" | "handoff" | "archived-handoff";
|
|
4
|
+
export type RecommendationClaimSource = "priority" | "active-task" | "resume-deviation" | "dependency-ready" | "handoff" | "archived-handoff" | "on-hold";
|
|
4
5
|
export interface RecommendationClaim {
|
|
5
6
|
kind: RecommendationClaimKind;
|
|
6
7
|
source: RecommendationClaimSource;
|
|
@@ -46,6 +47,8 @@ export interface NextWorkRecommendation {
|
|
|
46
47
|
nextPhase: WorkSummary | null;
|
|
47
48
|
nextTask: WorkSummary | null;
|
|
48
49
|
claims: RecommendationClaim[];
|
|
50
|
+
/** On-hold tasks in started phases, ready-to-resume first (see on-hold.ts). */
|
|
51
|
+
onHold: OnHoldTask[];
|
|
49
52
|
}
|
|
50
53
|
export interface TaskRecommendation {
|
|
51
54
|
kind: TaskRecommendationKind;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-selection.d.ts","sourceRoot":"","sources":["../src/task-selection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"task-selection.d.ts","sourceRoot":"","sources":["../src/task-selection.ts"],"names":[],"mappings":"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"}
|
package/dist/task-selection.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { listOnHoldWork, startedOpenPhaseIds } from "./on-hold.js";
|
|
1
2
|
const hardUnavailable = new Set(["blocked", "deferred", "canceled", "rejected"]);
|
|
2
3
|
const terminal = new Set(["done", "canceled", "rejected"]);
|
|
3
4
|
const priority = (entity) => entity.priority != null ? entity.priority : Number.MAX_SAFE_INTEGER;
|
|
@@ -15,8 +16,19 @@ const selectionClaimKind = (selectionKind) => {
|
|
|
15
16
|
return "insufficient";
|
|
16
17
|
return "advisory";
|
|
17
18
|
};
|
|
18
|
-
const buildRecommendationClaims = (selection, evidence = {}, dependencyReadyCandidates = []) => {
|
|
19
|
+
const buildRecommendationClaims = (selection, evidence = {}, dependencyReadyCandidates = [], onHold = []) => {
|
|
19
20
|
const claims = [];
|
|
21
|
+
// Parked work whose every named blocker is finished competes with fresh
|
|
22
|
+
// work; listed before it so the claim limit never drops it.
|
|
23
|
+
const readyOnHold = onHold.filter((item) => item.readyToResume).map((item) => ({
|
|
24
|
+
kind: "advisory",
|
|
25
|
+
source: "on-hold",
|
|
26
|
+
ref: item.ref,
|
|
27
|
+
title: item.title,
|
|
28
|
+
reason: `On hold (${item.status}); everything its reason names is finished (${item.namedRefs.map((named) => named.ref).join(", ")}), so it may be ready. Re-check it and resume it if nothing else holds it.`,
|
|
29
|
+
taskId: item.taskId,
|
|
30
|
+
phaseId: item.phaseId,
|
|
31
|
+
}));
|
|
20
32
|
if (selection.candidate) {
|
|
21
33
|
claims.push({
|
|
22
34
|
kind: selectionClaimKind(selection.kind),
|
|
@@ -29,6 +41,7 @@ const buildRecommendationClaims = (selection, evidence = {}, dependencyReadyCand
|
|
|
29
41
|
...(selection.candidate.feature?.id ? { featureId: selection.candidate.feature.id } : {}),
|
|
30
42
|
});
|
|
31
43
|
}
|
|
44
|
+
claims.push(...readyOnHold);
|
|
32
45
|
for (const candidate of dependencyReadyCandidates) {
|
|
33
46
|
claims.push({
|
|
34
47
|
kind: "advisory",
|
|
@@ -251,6 +264,12 @@ export function recommendNextWork(features, phases, deviations = [], currentPhas
|
|
|
251
264
|
&& !(feature && hardUnavailable.has(feature.status))
|
|
252
265
|
&& task.dependsOn.every((id) => taskById.get(id)?.status === "done"))
|
|
253
266
|
.sort((left, right) => compare(left.feature ?? { priority: 0, number: 0 }, right.feature ?? { priority: 0, number: 0 }) || compare(left.phase, right.phase) || compare(left.task, right.task));
|
|
267
|
+
// On-hold work in phases already under way, plus the phase the
|
|
268
|
+
// recommendation is about to send the agent into.
|
|
269
|
+
const onHoldPhaseIds = startedOpenPhaseIds(phases);
|
|
270
|
+
if (selection.candidate)
|
|
271
|
+
onHoldPhaseIds.add(selection.candidate.phase.id);
|
|
272
|
+
const onHold = listOnHoldWork(features, phases, onHoldPhaseIds);
|
|
254
273
|
const activeTask = selection.kind === "active" && selection.candidate
|
|
255
274
|
? summarizeTask(selection.candidate.task)
|
|
256
275
|
: null;
|
|
@@ -260,7 +279,8 @@ export function recommendNextWork(features, phases, deviations = [], currentPhas
|
|
|
260
279
|
nextFeature: selection.candidate?.feature ? summarizeFeature(selection.candidate.feature) : null,
|
|
261
280
|
nextPhase: selection.candidate ? summarizePhase(selection.candidate.phase) : null,
|
|
262
281
|
nextTask: selection.candidate ? summarizeTask(selection.candidate.task) : null,
|
|
263
|
-
claims: buildRecommendationClaims(selection, evidence, dependencyReadyCandidates),
|
|
282
|
+
claims: buildRecommendationClaims(selection, evidence, dependencyReadyCandidates, onHold),
|
|
283
|
+
onHold,
|
|
264
284
|
};
|
|
265
285
|
}
|
|
266
286
|
/**
|
package/package.json
CHANGED
package/planner-skill.md
CHANGED
|
@@ -86,6 +86,7 @@ A decision has exactly one owner: the project when it is project-wide, a feature
|
|
|
86
86
|
- A temporary task completion can emit `RESUME REQUIRED`. Resume the preserved source or deliberately switch again; do not silently abandon it.
|
|
87
87
|
- Complete a task only after implementation and verification. Supply durable completion evidence, files touched, decisions, remaining or unverified work, and updated code references.
|
|
88
88
|
- Status changes to `blocked`, `canceled`, `rejected`, `deferred`, `waiting`, or back to `planned` require a substantive motivation on agent surfaces.
|
|
89
|
+
- A task that belongs in a different phase is moved with `task_move` / `planner-task-move`, never recreated. Recreating loses the id, statusLog, checklist, accepted decisions, pause history, and every `dependsOn` edge pointing at it; moving keeps all of it and only the composite ref changes. Pass refs only — the move never needs the task's description.
|
|
89
90
|
|
|
90
91
|
## Mutation integrity and oversized descriptions
|
|
91
92
|
|
|
@@ -217,7 +218,7 @@ The MCP adapter publishes these tools:
|
|
|
217
218
|
- Project: `planner-project-language`, `planner-project-discuss`, `planner-project-guidelines-show`, `planner-project-guidelines-update`, `planner-project-context-migrate`, `planner-accepted-decision-create`, `planner-accepted-decision-update`, `planner-accepted-decision-delete`, `planner-requirement-list`, `planner-requirement-create`, `planner-requirement-update`, `planner-requirement-delete`.
|
|
218
219
|
- Features: `planner-feature-list`, `planner-feature-add`, `planner-feature-show`, `planner-feature-discuss`, `planner-feature-update`, `planner-feature-delete`.
|
|
219
220
|
- Phases: `planner-phase-list`, `planner-phase-add`, `planner-phase-show`, `planner-phase-discuss`, `planner-phase-update`, `planner-phase-delete`.
|
|
220
|
-
- Tasks: `planner-task-list`, `planner-task-add`, `planner-task-show`, `planner-task-discuss`, `planner-task-update`, `planner-task-dependency-add`, `planner-task-dependency-delete`, `planner-task-delete`, `planner-task-recommend`, `planner-task-deviation`, `planner-task-pause`, `planner-task-switch`, `planner-task-start`, `planner-task-reopen`, `planner-task-complete`, `planner-task-checklist-toggle`, `planner-task-checklist-add`, `planner-task-checklist-remove`.
|
|
221
|
+
- Tasks: `planner-task-list`, `planner-task-add`, `planner-task-show`, `planner-task-discuss`, `planner-task-update`, `planner-task-move`, `planner-task-dependency-add`, `planner-task-dependency-delete`, `planner-task-delete`, `planner-task-recommend`, `planner-task-deviation`, `planner-task-pause`, `planner-task-switch`, `planner-task-start`, `planner-task-reopen`, `planner-task-complete`, `planner-task-checklist-toggle`, `planner-task-checklist-add`, `planner-task-checklist-remove`.
|
|
221
222
|
- Handoffs: `planner-handoff-list`, `planner-handoff-show`, `planner-handoff-prepare`, `planner-handoff-write`, `planner-handoff-verify`, `planner-handoff-clear`.
|
|
222
223
|
|
|
223
224
|
## Pi tool inventory
|
|
@@ -229,7 +230,7 @@ The Pi adapter registers these tools:
|
|
|
229
230
|
- Plan: `plan_init`, `plan_get`, `description_freshness`, `plan_render`, `plan_repair`, `plan_cleanup_orphan_phases`, `plan_authorize_bypass`, `plan_clear_bypass`.
|
|
230
231
|
- Features: `feature_list`, `feature_get`, `feature_create`, `feature_discuss`, `feature_update`, `feature_delete`.
|
|
231
232
|
- Phases: `phase_list`, `phase_get`, `phase_create`, `phase_discuss`, `phase_update`, `phase_delete`, `decision_record` (deprecated, write-disabled compatibility redirect — see Accepted Decision ownership).
|
|
232
|
-
- Tasks: `task_list`, `task_get`, `task_create`, `task_update`, `task_dependency_add`, `task_dependency_delete`, `task_delete`, `task_recommend`, `task_deviation`, `task_pause`, `task_switch`, `task_start`, `task_reopen`, `task_complete`, `task_checklist_toggle`, `task_checklist_add`, `task_checklist_remove`.
|
|
233
|
+
- Tasks: `task_list`, `task_get`, `task_create`, `task_update`, `task_move`, `task_dependency_add`, `task_dependency_delete`, `task_delete`, `task_recommend`, `task_deviation`, `task_pause`, `task_switch`, `task_start`, `task_reopen`, `task_complete`, `task_checklist_toggle`, `task_checklist_add`, `task_checklist_remove`.
|
|
233
234
|
- Handoffs: `handoff_list`, `handoff_show`, `handoff_prepare`, `handoff_write`, `handoff_verify`, `handoff_clear`.
|
|
234
235
|
- Dashboard and lifecycle: `planner-web`, `planner-load`, `planner-stop`.
|
|
235
236
|
- Deprecated compatibility aliases: `plan_get_handoff`, `plan_write_handoff`, `plan_delete_handoff`. Prefer the entity-scoped handoff tools.
|