@veewo/claw-core 0.1.75 → 0.1.76
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/src/context.js +45 -4
- package/dist/src/context.js.map +1 -1
- package/dist/src/effective-config.js +8 -0
- package/dist/src/effective-config.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init.d.ts +1 -2
- package/dist/src/init.js +5 -3
- package/dist/src/init.js.map +1 -1
- package/dist/src/knowledge-sidecar.d.ts +88 -0
- package/dist/src/knowledge-sidecar.js +327 -0
- package/dist/src/knowledge-sidecar.js.map +1 -0
- package/dist/src/plan-templates.js +73 -2
- package/dist/src/plan-templates.js.map +1 -1
- package/dist/src/plan.js +375 -118
- package/dist/src/plan.js.map +1 -1
- package/dist/src/project-check.js +29 -12
- package/dist/src/project-check.js.map +1 -1
- package/dist/src/templates/plans/default.js +2 -2
- package/dist/src/templates/plans/default.js.map +1 -1
- package/dist/src/types.d.ts +69 -7
- package/dist/src/workflow-guidance.config.json +40 -59
- package/dist/src/workflow-guidance.d.ts +1 -0
- package/dist/src/workflow-guidance.js +17 -21
- package/dist/src/workflow-guidance.js.map +1 -1
- package/package.json +1 -1
package/dist/src/plan.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ensureTaskContext, removeLegacyTaskMeta, resolveProjectContext, resolve
|
|
|
6
6
|
import { resolvePlanEffectiveConfig } from "./effective-config.js";
|
|
7
7
|
import { ClawError } from "./errors.js";
|
|
8
8
|
import { readJsonFile, withFileLock, withSerializedAccess, writeJsonFile } from "./io.js";
|
|
9
|
+
import { tryCompleteKnowledgePlan, tryRegisterKnowledgePlan } from "./knowledge-sidecar.js";
|
|
9
10
|
import { buildPlanEvent } from "./plan-events.js";
|
|
10
11
|
import { isProcessStatus, } from "./requirements-gate.js";
|
|
11
12
|
import { buildPlanViewModel } from "./plan-view.js";
|
|
@@ -82,6 +83,11 @@ export async function writePlan(input) {
|
|
|
82
83
|
writeJsonFile(planPath, plan);
|
|
83
84
|
});
|
|
84
85
|
bindSessionToPlan(project, input.ownerSessionKey, planPath);
|
|
86
|
+
tryRegisterKnowledgePlan({
|
|
87
|
+
project,
|
|
88
|
+
sessionId: input.ownerSessionKey,
|
|
89
|
+
planPath,
|
|
90
|
+
});
|
|
85
91
|
if (input.ownerSessionKey) {
|
|
86
92
|
removeLegacyTaskMeta(task);
|
|
87
93
|
}
|
|
@@ -134,9 +140,6 @@ export async function editPlan(input) {
|
|
|
134
140
|
planFile,
|
|
135
141
|
});
|
|
136
142
|
}
|
|
137
|
-
if (input.patch?.tasks !== undefined && (input.taskId !== undefined || input.taskStatus !== undefined)) {
|
|
138
|
-
throw new ClawError("PROJECT_CONFIG_INVALID", "patch.tasks cannot be combined with taskId/taskStatus updates in the same plan edit. Update tasks and task progress in separate commands.");
|
|
139
|
-
}
|
|
140
143
|
return withSerializedAccess(planPath, async () => {
|
|
141
144
|
const previous = normalizePlanDocument(readJsonFile(planPath));
|
|
142
145
|
const previousStatus = previous.status;
|
|
@@ -146,107 +149,180 @@ export async function editPlan(input) {
|
|
|
146
149
|
const changedTaskIds = [];
|
|
147
150
|
const appendedTaskIds = [];
|
|
148
151
|
const completedTaskIds = [];
|
|
149
|
-
|
|
152
|
+
let next = structuredClone(previous);
|
|
150
153
|
const requestedStatus = input.planStatus ? normalizePlanStatus(input.planStatus) : undefined;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const taskItem of appendedTasks) {
|
|
174
|
-
if (currentIds.has(taskItem.id)) {
|
|
175
|
-
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${taskItem.id} already exists in plan.`);
|
|
154
|
+
let operationChain;
|
|
155
|
+
if (input.operations) {
|
|
156
|
+
const chain = await applyPlanMutationOperations({
|
|
157
|
+
projectRoot: task.project.projectRoot,
|
|
158
|
+
initialPlan: previous,
|
|
159
|
+
operations: input.operations,
|
|
160
|
+
});
|
|
161
|
+
next = chain.plan;
|
|
162
|
+
operationChain = chain.result;
|
|
163
|
+
const previousTasks = new Map(previous.tasks.map((item) => [item.id, item]));
|
|
164
|
+
const nextTasks = new Map(next.tasks.map((item) => [item.id, item]));
|
|
165
|
+
for (const [id, taskItem] of nextTasks) {
|
|
166
|
+
const prior = previousTasks.get(id);
|
|
167
|
+
if (!prior) {
|
|
168
|
+
appendedTaskIds.push(id);
|
|
169
|
+
changedTaskIds.push(id);
|
|
170
|
+
}
|
|
171
|
+
else if (JSON.stringify(prior) !== JSON.stringify(taskItem)) {
|
|
172
|
+
changedTaskIds.push(id);
|
|
173
|
+
if (prior.status !== "done" && taskItem.status === "done") {
|
|
174
|
+
completedTaskIds.push(id);
|
|
175
|
+
}
|
|
176
176
|
}
|
|
177
|
-
validatePlanTask(taskItem);
|
|
178
|
-
next.tasks.push(taskItem);
|
|
179
|
-
currentIds.add(taskItem.id);
|
|
180
|
-
appendedTaskIds.push(taskItem.id);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
next.requirements = normalizePlanRequirements(next.requirements);
|
|
184
|
-
next.tasks = normalizePlanTasks(Array.isArray(next.tasks) ? next.tasks : []);
|
|
185
|
-
if (input.taskId !== undefined || input.taskStatus !== undefined) {
|
|
186
|
-
if (input.taskId === undefined || input.taskStatus === undefined) {
|
|
187
|
-
throw new ClawError("PROJECT_CONFIG_INVALID", "taskId and taskStatus must be provided together when updating a plan task status.");
|
|
188
|
-
}
|
|
189
|
-
if (!isProcess(next.status)) {
|
|
190
|
-
throw new ClawError("TASK_STATUS_FORBIDDEN_IN_NON_ACTIVE_PLAN", "Task progress can only be updated while plan.status is process.*. If requirements are already confirmed, move the plan to process.active first.", {
|
|
191
|
-
planStatus: next.status,
|
|
192
|
-
suggestedCommand: `claw plan edit --task ${task.taskName}${planFile === "plan.json" ? "" : ` --plan ${planFile}`} --plan-status process.active`,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
const planTask = next.tasks.find((item) => item.id === input.taskId);
|
|
196
|
-
if (!planTask) {
|
|
197
|
-
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${input.taskId} was not found in this plan.`);
|
|
198
|
-
}
|
|
199
|
-
validatePlanTaskStatus(input.taskStatus);
|
|
200
|
-
const previousTaskStatus = planTask.status;
|
|
201
|
-
planTask.status = input.taskStatus;
|
|
202
|
-
if (input.taskChoiceId !== undefined) {
|
|
203
|
-
planTask.choiceId = input.taskChoiceId;
|
|
204
177
|
}
|
|
205
|
-
|
|
206
|
-
|
|
178
|
+
for (const id of previousTasks.keys()) {
|
|
179
|
+
if (!nextTasks.has(id)) {
|
|
180
|
+
changedTaskIds.push(id);
|
|
181
|
+
}
|
|
207
182
|
}
|
|
208
|
-
|
|
209
|
-
if (previousTaskStatus !== "done" && input.taskStatus === "done") {
|
|
210
|
-
completedTaskIds.push(planTask.id);
|
|
183
|
+
for (const taskId of completedTaskIds) {
|
|
211
184
|
events.push(buildPlanEvent("plan_task_completed", {
|
|
212
185
|
mutationId,
|
|
213
186
|
commandSource,
|
|
214
187
|
planPath,
|
|
215
188
|
planTitle: next.title,
|
|
216
189
|
planStatus: next.status,
|
|
217
|
-
taskId
|
|
218
|
-
affectedPlanTaskIds: [
|
|
190
|
+
taskId,
|
|
191
|
+
affectedPlanTaskIds: [taskId],
|
|
219
192
|
}));
|
|
220
193
|
}
|
|
221
194
|
}
|
|
222
|
-
|
|
223
|
-
if (requestedStatus
|
|
224
|
-
|
|
195
|
+
else {
|
|
196
|
+
if (requestedStatus) {
|
|
197
|
+
const validation = canSetPlanStatus(previousStatus, requestedStatus);
|
|
198
|
+
if (!validation.ok) {
|
|
199
|
+
throw new ClawError(validation.code, validation.error);
|
|
200
|
+
}
|
|
201
|
+
next.status = requestedStatus;
|
|
202
|
+
}
|
|
203
|
+
if (input.updates) {
|
|
204
|
+
applyPlanFieldUpdates(next, input.updates);
|
|
205
|
+
}
|
|
206
|
+
if (previousStatus !== "end.completed" && next.status === "end.completed") {
|
|
207
|
+
next.completedAt = new Date().toISOString();
|
|
225
208
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
"Enter process.active",
|
|
229
|
-
]);
|
|
230
|
-
const lifecycleTasks = next.tasks.filter((taskItem) => lifecycleTitles.has(taskItem.title));
|
|
231
|
-
if (lifecycleTasks.length !== lifecycleTitles.size) {
|
|
232
|
-
throw new ClawError("PROJECT_CONFIG_INVALID", "Atomic plan start is only available when both default lifecycle bridge tasks are present.");
|
|
209
|
+
else if (previousStatus === "end.completed" && next.status !== "end.completed") {
|
|
210
|
+
delete next.completedAt;
|
|
233
211
|
}
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
212
|
+
if (input.appendTasks?.length) {
|
|
213
|
+
if (isEnd(previous.status) && !requestedStatus) {
|
|
214
|
+
next.status = "prepare.requirements";
|
|
215
|
+
}
|
|
216
|
+
const currentIds = new Set(next.tasks.map((taskItem) => taskItem.id));
|
|
217
|
+
const appendedTasks = normalizePlanTasks(input.appendTasks, nextAvailableTaskId(next.tasks));
|
|
218
|
+
for (const taskItem of appendedTasks) {
|
|
219
|
+
if (currentIds.has(taskItem.id)) {
|
|
220
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${taskItem.id} already exists in plan.`);
|
|
221
|
+
}
|
|
222
|
+
validatePlanTask(taskItem);
|
|
223
|
+
next.tasks.push(taskItem);
|
|
224
|
+
currentIds.add(taskItem.id);
|
|
225
|
+
appendedTaskIds.push(taskItem.id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (input.removeTaskIds?.length) {
|
|
229
|
+
const removeIds = new Set(input.removeTaskIds);
|
|
230
|
+
for (const taskId of removeIds) {
|
|
231
|
+
if (!next.tasks.some((taskItem) => taskItem.id === taskId)) {
|
|
232
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${taskId} was not found in this plan.`);
|
|
233
|
+
}
|
|
234
|
+
if (input.taskId === taskId) {
|
|
235
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${taskId} cannot be updated and removed in the same plan edit.`);
|
|
236
|
+
}
|
|
237
|
+
changedTaskIds.push(taskId);
|
|
238
|
+
}
|
|
239
|
+
next.tasks = next.tasks.filter((taskItem) => !removeIds.has(taskItem.id));
|
|
240
|
+
}
|
|
241
|
+
next.requirements = normalizePlanRequirements(next.requirements);
|
|
242
|
+
next.tasks = normalizePlanTasks(Array.isArray(next.tasks) ? next.tasks : []);
|
|
243
|
+
const hasTaskMutation = input.taskId !== undefined
|
|
244
|
+
|| input.taskStatus !== undefined
|
|
245
|
+
|| input.taskChoiceId !== undefined
|
|
246
|
+
|| input.taskTitle !== undefined
|
|
247
|
+
|| input.taskDetail !== undefined;
|
|
248
|
+
if (hasTaskMutation) {
|
|
249
|
+
if (input.taskId === undefined) {
|
|
250
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "taskId is required when updating a plan task.");
|
|
251
|
+
}
|
|
252
|
+
if (input.taskStatus !== undefined && !isProcess(next.status)) {
|
|
253
|
+
throw new ClawError("TASK_STATUS_FORBIDDEN_IN_NON_ACTIVE_PLAN", "Task progress can only be updated while plan.status is process.*. If requirements are already confirmed, move the plan to process.active first.", {
|
|
254
|
+
planStatus: next.status,
|
|
255
|
+
suggestedCommand: "claw plan resume",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const planTask = next.tasks.find((item) => item.id === input.taskId);
|
|
259
|
+
if (!planTask) {
|
|
260
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${input.taskId} was not found in this plan.`);
|
|
261
|
+
}
|
|
262
|
+
const previousTaskStatus = planTask.status;
|
|
263
|
+
if (input.taskTitle !== undefined) {
|
|
264
|
+
planTask.title = input.taskTitle;
|
|
265
|
+
}
|
|
266
|
+
if (input.taskDetail !== undefined) {
|
|
267
|
+
planTask.detail = input.taskDetail;
|
|
268
|
+
}
|
|
269
|
+
if (input.taskStatus !== undefined) {
|
|
270
|
+
validatePlanTaskStatus(input.taskStatus);
|
|
271
|
+
planTask.status = input.taskStatus;
|
|
272
|
+
if (input.taskChoiceId !== undefined) {
|
|
273
|
+
planTask.choiceId = input.taskChoiceId;
|
|
274
|
+
}
|
|
275
|
+
else if (input.taskStatus !== "done") {
|
|
276
|
+
delete planTask.choiceId;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else if (input.taskChoiceId !== undefined) {
|
|
280
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "taskChoiceId requires taskStatus=done in the same plan edit.");
|
|
281
|
+
}
|
|
282
|
+
validatePlanTask(planTask);
|
|
283
|
+
changedTaskIds.push(planTask.id);
|
|
284
|
+
if (previousTaskStatus !== "done" && planTask.status === "done") {
|
|
285
|
+
completedTaskIds.push(planTask.id);
|
|
286
|
+
events.push(buildPlanEvent("plan_task_completed", {
|
|
287
|
+
mutationId,
|
|
288
|
+
commandSource,
|
|
289
|
+
planPath,
|
|
290
|
+
planTitle: next.title,
|
|
291
|
+
planStatus: next.status,
|
|
292
|
+
taskId: planTask.id,
|
|
293
|
+
affectedPlanTaskIds: [planTask.id],
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (input.completeLifecycleBridge) {
|
|
298
|
+
if (requestedStatus !== "process.active") {
|
|
299
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "Completing the lifecycle bridge requires planStatus=process.active.");
|
|
300
|
+
}
|
|
301
|
+
const lifecycleTitles = new Set([
|
|
302
|
+
"Analyze the request and fill executable tasks with the planning skill",
|
|
303
|
+
"Enter process.active",
|
|
304
|
+
]);
|
|
305
|
+
const lifecycleTasks = next.tasks.filter((taskItem) => lifecycleTitles.has(taskItem.title));
|
|
306
|
+
if (lifecycleTasks.length !== lifecycleTitles.size) {
|
|
307
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "Atomic plan start is only available when both default lifecycle bridge tasks are present.");
|
|
308
|
+
}
|
|
309
|
+
for (const lifecycleTask of lifecycleTasks) {
|
|
310
|
+
if (lifecycleTask.status === "done") {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
lifecycleTask.status = "done";
|
|
314
|
+
changedTaskIds.push(lifecycleTask.id);
|
|
315
|
+
completedTaskIds.push(lifecycleTask.id);
|
|
316
|
+
events.push(buildPlanEvent("plan_task_completed", {
|
|
317
|
+
mutationId,
|
|
318
|
+
commandSource,
|
|
319
|
+
planPath,
|
|
320
|
+
planTitle: next.title,
|
|
321
|
+
planStatus: next.status,
|
|
322
|
+
taskId: lifecycleTask.id,
|
|
323
|
+
affectedPlanTaskIds: [lifecycleTask.id],
|
|
324
|
+
}));
|
|
237
325
|
}
|
|
238
|
-
lifecycleTask.status = "done";
|
|
239
|
-
changedTaskIds.push(lifecycleTask.id);
|
|
240
|
-
completedTaskIds.push(lifecycleTask.id);
|
|
241
|
-
events.push(buildPlanEvent("plan_task_completed", {
|
|
242
|
-
mutationId,
|
|
243
|
-
commandSource,
|
|
244
|
-
planPath,
|
|
245
|
-
planTitle: next.title,
|
|
246
|
-
planStatus: next.status,
|
|
247
|
-
taskId: lifecycleTask.id,
|
|
248
|
-
affectedPlanTaskIds: [lifecycleTask.id],
|
|
249
|
-
}));
|
|
250
326
|
}
|
|
251
327
|
}
|
|
252
328
|
await validateDoneTransitions({
|
|
@@ -265,12 +341,12 @@ export async function editPlan(input) {
|
|
|
265
341
|
taskName: task.taskName,
|
|
266
342
|
planFile,
|
|
267
343
|
planPath,
|
|
268
|
-
suggestedCommand: `claw plan show --task ${task.taskName}${planFile === "plan.json" ? "" : ` --plan ${planFile}`}`,
|
|
344
|
+
suggestedCommand: `claw plan show --task-name ${task.taskName}${planFile === "plan.json" ? "" : ` --plan-file ${planFile}`}`,
|
|
269
345
|
});
|
|
270
346
|
}
|
|
271
347
|
writeJsonFile(planPath, next);
|
|
272
348
|
});
|
|
273
|
-
if (previousStatus !== next.status || input.
|
|
349
|
+
if (previousStatus !== next.status || input.updates || changedTaskIds.length > 0 || input.appendTasks?.length || input.removeTaskIds?.length || (operationChain?.completedOperations ?? 0) > 0) {
|
|
274
350
|
events.unshift(buildPlanEvent("plan_changed", {
|
|
275
351
|
mutationId,
|
|
276
352
|
commandSource,
|
|
@@ -323,6 +399,22 @@ export async function editPlan(input) {
|
|
|
323
399
|
else {
|
|
324
400
|
bindSessionToPlan(task.project, input.ownerSessionKey, resultPlanPath);
|
|
325
401
|
}
|
|
402
|
+
if (completionHooks) {
|
|
403
|
+
tryCompleteKnowledgePlan({
|
|
404
|
+
project: task.project,
|
|
405
|
+
sessionId: input.ownerSessionKey,
|
|
406
|
+
completedPlanPath: planPath,
|
|
407
|
+
...(completionHooks.subplanClosureCandidate ? { resumedPlanPath: resultPlanPath } : {}),
|
|
408
|
+
completedAt: next.completedAt,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
else if (!resultPlan.status.startsWith("end.")) {
|
|
412
|
+
tryRegisterKnowledgePlan({
|
|
413
|
+
project: task.project,
|
|
414
|
+
sessionId: input.ownerSessionKey,
|
|
415
|
+
planPath: resultPlanPath,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
326
418
|
if (input.ownerSessionKey) {
|
|
327
419
|
removeLegacyTaskMeta(task);
|
|
328
420
|
}
|
|
@@ -350,6 +442,7 @@ export async function editPlan(input) {
|
|
|
350
442
|
previousStatus,
|
|
351
443
|
completionHooks,
|
|
352
444
|
changedTaskIds,
|
|
445
|
+
appendedTaskIds,
|
|
353
446
|
completedTaskIds,
|
|
354
447
|
}),
|
|
355
448
|
}),
|
|
@@ -360,6 +453,7 @@ export async function editPlan(input) {
|
|
|
360
453
|
plan: resultPlan,
|
|
361
454
|
}),
|
|
362
455
|
events,
|
|
456
|
+
...(operationChain ? { operationChain } : {}),
|
|
363
457
|
};
|
|
364
458
|
});
|
|
365
459
|
}
|
|
@@ -522,41 +616,204 @@ function validatePlanTaskStatus(status) {
|
|
|
522
616
|
throw new ClawError("PROJECT_CONFIG_INVALID", `Unsupported plan task status "${status}". Canonical values are pending, in_progress, subagent_running, done, blocked.`);
|
|
523
617
|
}
|
|
524
618
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
619
|
+
async function applyPlanMutationOperations(input) {
|
|
620
|
+
let current = structuredClone(input.initialPlan);
|
|
621
|
+
let completedOperations = 0;
|
|
622
|
+
let failedOperation;
|
|
623
|
+
for (let index = 0; index < input.operations.length; index += 1) {
|
|
624
|
+
const operation = input.operations[index];
|
|
625
|
+
const candidate = structuredClone(current);
|
|
626
|
+
try {
|
|
627
|
+
switch (operation.type) {
|
|
628
|
+
case "plan.update":
|
|
629
|
+
applyPlanFieldUpdates(candidate, operation.updates);
|
|
630
|
+
break;
|
|
631
|
+
case "plan.status": {
|
|
632
|
+
const status = normalizePlanStatus(operation.status);
|
|
633
|
+
if (!status) {
|
|
634
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Unsupported plan status "${operation.status}".`);
|
|
635
|
+
}
|
|
636
|
+
const validation = canSetPlanStatus(current.status, status);
|
|
637
|
+
if (!validation.ok) {
|
|
638
|
+
throw new ClawError(validation.code, validation.error);
|
|
639
|
+
}
|
|
640
|
+
candidate.status = status;
|
|
641
|
+
if (current.status !== "end.completed" && status === "end.completed") {
|
|
642
|
+
candidate.completedAt = new Date().toISOString();
|
|
643
|
+
}
|
|
644
|
+
else if (current.status === "end.completed" && status !== "end.completed") {
|
|
645
|
+
delete candidate.completedAt;
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
case "task.add": {
|
|
650
|
+
if (isEnd(current.status)) {
|
|
651
|
+
candidate.status = "prepare.requirements";
|
|
652
|
+
delete candidate.completedAt;
|
|
653
|
+
}
|
|
654
|
+
const [task] = normalizePlanTasks([{
|
|
655
|
+
title: operation.title,
|
|
656
|
+
...(operation.detail !== undefined ? { detail: operation.detail } : {}),
|
|
657
|
+
status: "pending",
|
|
658
|
+
}], nextAvailableTaskId(candidate.tasks));
|
|
659
|
+
validatePlanTask(task);
|
|
660
|
+
candidate.tasks.push(task);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
case "task.edit": {
|
|
664
|
+
if (operation.status !== undefined && !isProcess(candidate.status)) {
|
|
665
|
+
throw new ClawError("TASK_STATUS_FORBIDDEN_IN_NON_ACTIVE_PLAN", "Task progress can only be updated while plan.status is process.*. If requirements are already confirmed, move the plan to process.active first.", { planStatus: candidate.status, suggestedCommand: "claw plan resume" });
|
|
666
|
+
}
|
|
667
|
+
const task = candidate.tasks.find((item) => item.id === operation.id);
|
|
668
|
+
if (!task) {
|
|
669
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${operation.id} was not found in this plan.`);
|
|
670
|
+
}
|
|
671
|
+
if (operation.title !== undefined)
|
|
672
|
+
task.title = operation.title;
|
|
673
|
+
if (operation.detail !== undefined)
|
|
674
|
+
task.detail = operation.detail;
|
|
675
|
+
if (operation.status !== undefined) {
|
|
676
|
+
validatePlanTaskStatus(operation.status);
|
|
677
|
+
task.status = operation.status;
|
|
678
|
+
if (operation.choiceId !== undefined) {
|
|
679
|
+
task.choiceId = operation.choiceId;
|
|
680
|
+
}
|
|
681
|
+
else if (operation.status !== "done") {
|
|
682
|
+
delete task.choiceId;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
else if (operation.choiceId !== undefined) {
|
|
686
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "taskChoiceId requires taskStatus=done in the same task edit group.");
|
|
687
|
+
}
|
|
688
|
+
validatePlanTask(task);
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case "task.remove": {
|
|
692
|
+
const taskIndex = candidate.tasks.findIndex((item) => item.id === operation.id);
|
|
693
|
+
if (taskIndex < 0) {
|
|
694
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${operation.id} was not found in this plan.`);
|
|
695
|
+
}
|
|
696
|
+
candidate.tasks.splice(taskIndex, 1);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
542
699
|
}
|
|
543
|
-
|
|
544
|
-
|
|
700
|
+
candidate.requirements = normalizePlanRequirements(candidate.requirements);
|
|
701
|
+
candidate.tasks = normalizePlanTasks(candidate.tasks);
|
|
702
|
+
await validateDoneTransitions({
|
|
703
|
+
projectRoot: input.projectRoot,
|
|
704
|
+
previousPlan: current,
|
|
705
|
+
nextPlan: candidate,
|
|
706
|
+
});
|
|
707
|
+
validatePlanDocument(candidate);
|
|
708
|
+
if (candidate.status === "end.completed" && !candidate.retrospective?.summary?.trim()) {
|
|
709
|
+
throw new ClawError("RETROSPECTIVE_REQUIRED", "end.completed requires retrospective.summary before the plan can be completed.");
|
|
545
710
|
}
|
|
711
|
+
current = candidate;
|
|
712
|
+
completedOperations += 1;
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
const clawError = error instanceof ClawError
|
|
716
|
+
? error
|
|
717
|
+
: new ClawError("PROJECT_CONFIG_INVALID", error instanceof Error ? error.message : String(error));
|
|
718
|
+
failedOperation = {
|
|
719
|
+
index,
|
|
720
|
+
type: operation.type,
|
|
721
|
+
error: {
|
|
722
|
+
code: clawError.code,
|
|
723
|
+
message: clawError.message,
|
|
724
|
+
...(clawError.details ? { details: clawError.details } : {}),
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
break;
|
|
546
728
|
}
|
|
547
|
-
return base;
|
|
548
729
|
}
|
|
549
|
-
return
|
|
730
|
+
return {
|
|
731
|
+
plan: current,
|
|
732
|
+
result: {
|
|
733
|
+
status: failedOperation ? "partial" : "completed",
|
|
734
|
+
completedOperations,
|
|
735
|
+
remainingOperations: input.operations.length - completedOperations - (failedOperation ? 1 : 0),
|
|
736
|
+
...(failedOperation ? { failedOperation } : {}),
|
|
737
|
+
},
|
|
738
|
+
};
|
|
550
739
|
}
|
|
551
|
-
function
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
740
|
+
function applyPlanFieldUpdates(target, updates) {
|
|
741
|
+
if (updates.goalText !== undefined) {
|
|
742
|
+
target.goal.text = updates.goalText;
|
|
743
|
+
}
|
|
744
|
+
if (updates.requirementsSummary !== undefined
|
|
745
|
+
|| updates.openQuestions?.length
|
|
746
|
+
|| updates.removeOpenQuestions?.length
|
|
747
|
+
|| updates.acceptanceCriteria?.length
|
|
748
|
+
|| updates.removeAcceptanceCriteria?.length) {
|
|
749
|
+
target.requirements = normalizePlanRequirements(target.requirements);
|
|
750
|
+
if (updates.requirementsSummary !== undefined) {
|
|
751
|
+
target.requirements.summary = updates.requirementsSummary;
|
|
752
|
+
}
|
|
753
|
+
if (updates.openQuestions?.length) {
|
|
754
|
+
target.requirements.openQuestions.push(...updates.openQuestions);
|
|
755
|
+
}
|
|
756
|
+
if (updates.removeOpenQuestions?.length) {
|
|
757
|
+
target.requirements.openQuestions = removeRequiredStrings(target.requirements.openQuestions, updates.removeOpenQuestions, "open question");
|
|
758
|
+
}
|
|
759
|
+
if (updates.acceptanceCriteria?.length) {
|
|
760
|
+
target.requirements.acceptanceCriteria.push(...updates.acceptanceCriteria);
|
|
761
|
+
}
|
|
762
|
+
if (updates.removeAcceptanceCriteria?.length) {
|
|
763
|
+
target.requirements.acceptanceCriteria = removeRequiredStrings(target.requirements.acceptanceCriteria, updates.removeAcceptanceCriteria, "acceptance criterion");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (updates.planSummary !== undefined) {
|
|
767
|
+
target.summary = updates.planSummary;
|
|
768
|
+
}
|
|
769
|
+
if (updates.rules?.length) {
|
|
770
|
+
target.rules = [...(target.rules ?? []), ...updates.rules];
|
|
771
|
+
}
|
|
772
|
+
if (updates.removeRules?.length) {
|
|
773
|
+
target.rules = removeRequiredStrings(target.rules ?? [], updates.removeRules, "rule");
|
|
555
774
|
}
|
|
556
|
-
|
|
557
|
-
|
|
775
|
+
if (updates.keyDecisions?.length) {
|
|
776
|
+
target.keyDecisions = [...(target.keyDecisions ?? []), ...updates.keyDecisions];
|
|
777
|
+
}
|
|
778
|
+
if (updates.removeKeyDecisions?.length) {
|
|
779
|
+
target.keyDecisions = removeRequiredStrings(target.keyDecisions ?? [], updates.removeKeyDecisions, "key decision");
|
|
780
|
+
}
|
|
781
|
+
if (updates.references?.length) {
|
|
782
|
+
target.references = [...(target.references ?? []), ...updates.references];
|
|
783
|
+
}
|
|
784
|
+
if (updates.removeReferencePaths?.length) {
|
|
785
|
+
const currentReferences = target.references ?? [];
|
|
786
|
+
const retainedPaths = removeRequiredStrings(currentReferences.map((item) => item.path), updates.removeReferencePaths, "reference path");
|
|
787
|
+
const retained = new Set(retainedPaths);
|
|
788
|
+
target.references = currentReferences.filter((item) => retained.has(item.path));
|
|
789
|
+
}
|
|
790
|
+
if (updates.retrospectiveSummary !== undefined
|
|
791
|
+
|| updates.whatWorked?.length
|
|
792
|
+
|| updates.issues?.length
|
|
793
|
+
|| updates.followUps?.length) {
|
|
794
|
+
target.retrospective = target.retrospective ?? { summary: "" };
|
|
795
|
+
if (updates.retrospectiveSummary !== undefined) {
|
|
796
|
+
target.retrospective.summary = updates.retrospectiveSummary;
|
|
797
|
+
}
|
|
798
|
+
if (updates.whatWorked?.length) {
|
|
799
|
+
target.retrospective.whatWorked = [...(target.retrospective.whatWorked ?? []), ...updates.whatWorked];
|
|
800
|
+
}
|
|
801
|
+
if (updates.issues?.length) {
|
|
802
|
+
target.retrospective.issues = [...(target.retrospective.issues ?? []), ...updates.issues];
|
|
803
|
+
}
|
|
804
|
+
if (updates.followUps?.length) {
|
|
805
|
+
target.retrospective.followUps = [...(target.retrospective.followUps ?? []), ...updates.followUps];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function removeRequiredStrings(current, requested, label) {
|
|
810
|
+
const requestedValues = [...new Set(requested)];
|
|
811
|
+
const missing = requestedValues.filter((value) => !current.includes(value));
|
|
812
|
+
if (missing.length > 0) {
|
|
813
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Cannot remove ${label}; exact value not found: ${missing.map((value) => JSON.stringify(value)).join(", ")}.`, { label, missing });
|
|
558
814
|
}
|
|
559
|
-
|
|
815
|
+
const removed = new Set(requestedValues);
|
|
816
|
+
return current.filter((value) => !removed.has(value));
|
|
560
817
|
}
|
|
561
818
|
async function createSeedPlan(projectRoot, projectConfig, templateName, taskName, title, goalText, status = "prepare.requirements", forcePlanning = false, host) {
|
|
562
819
|
const effectiveTemplateName = templateName?.trim() || projectConfig?.defaultPlanTemplate?.trim() || defaultPlanTemplateName();
|