@veewo/claw-core 0.1.62 → 0.1.63
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/README.md +16 -16
- package/dist/src/context.js +1 -0
- package/dist/src/context.js.map +1 -1
- package/dist/src/effective-config.d.ts +2 -0
- package/dist/src/effective-config.js +10 -0
- package/dist/src/effective-config.js.map +1 -0
- package/dist/src/embedding-config.d.ts +9 -0
- package/dist/src/embedding-config.js +21 -0
- package/dist/src/embedding-config.js.map +1 -0
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.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.js +1 -0
- package/dist/src/init.js.map +1 -1
- package/dist/src/io.d.ts +4 -0
- package/dist/src/io.js +77 -0
- package/dist/src/io.js.map +1 -1
- package/dist/src/paths.d.ts +1 -0
- package/dist/src/paths.js +25 -1
- package/dist/src/paths.js.map +1 -1
- package/dist/src/plan-templates.d.ts +25 -2
- package/dist/src/plan-templates.js +582 -28
- package/dist/src/plan-templates.js.map +1 -1
- package/dist/src/plan.js +301 -213
- package/dist/src/plan.js.map +1 -1
- package/dist/src/project-check.js +10 -0
- package/dist/src/project-check.js.map +1 -1
- package/dist/src/project-config-defaults.d.ts +6 -0
- package/dist/src/project-config-defaults.js +5 -0
- package/dist/src/project-config-defaults.js.map +1 -0
- package/dist/src/templates/plans/default.d.ts +44 -14
- package/dist/src/templates/plans/default.js +44 -10
- package/dist/src/templates/plans/default.js.map +1 -1
- package/dist/src/types.d.ts +15 -1
- package/dist/src/workflow-guidance.config.json +396 -395
- package/dist/src/workflow-guidance.d.ts +2 -1
- package/dist/src/workflow-guidance.js +166 -21
- package/dist/src/workflow-guidance.js.map +1 -1
- package/package.json +44 -44
package/dist/src/plan.js
CHANGED
|
@@ -2,12 +2,13 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildCompletionHooks } from "./completion-hooks.js";
|
|
4
4
|
import { ensureTaskMeta, resolveProjectContext, resolveTaskContext, resolveTaskName, saveTaskMeta } from "./context.js";
|
|
5
|
+
import { resolvePlanEffectiveConfig } from "./effective-config.js";
|
|
5
6
|
import { ClawError } from "./errors.js";
|
|
6
|
-
import { readJsonFile, withFileLock, writeJsonFile } from "./io.js";
|
|
7
|
+
import { readJsonFile, withFileLock, withSerializedAccess, writeJsonFile } from "./io.js";
|
|
7
8
|
import { buildPlanEvent } from "./plan-events.js";
|
|
8
9
|
import { isProcessStatus, } from "./requirements-gate.js";
|
|
9
10
|
import { buildPlanViewModel } from "./plan-view.js";
|
|
10
|
-
import { renderSeedTemplateText, resolveSeedPlanTemplate } from "./plan-templates.js";
|
|
11
|
+
import { getTemplateTaskDoneChoices, renderSeedTemplateText, resolveSeedPlanTemplate } from "./plan-templates.js";
|
|
11
12
|
import { ensureInsideDir, normalizePlanFile, slugFromFilePath } from "./paths.js";
|
|
12
13
|
import { buildGoalModeObjective, buildPlanWorkflowGuidance } from "./workflow-guidance.js";
|
|
13
14
|
const PLAN_STATUSES = [
|
|
@@ -98,12 +99,13 @@ export async function writePlan(input) {
|
|
|
98
99
|
eventType,
|
|
99
100
|
...(plan.parentPlan ? { parentPlan: plan.parentPlan } : {}),
|
|
100
101
|
...(plan.parentTaskId !== undefined ? { parentTaskId: plan.parentTaskId } : {}),
|
|
101
|
-
workflowGuidance: buildPlanWorkflowGuidance({
|
|
102
|
+
workflowGuidance: await buildPlanWorkflowGuidance({
|
|
102
103
|
taskName,
|
|
103
104
|
planFile,
|
|
104
105
|
plan,
|
|
105
106
|
commandSource: input.parentTaskId !== undefined ? "subplan.create" : "plan.create",
|
|
106
|
-
|
|
107
|
+
projectRoot: project.projectRoot,
|
|
108
|
+
projectConfig: resolvePlanEffectiveConfig(project.projectConfig, plan),
|
|
107
109
|
host: input.host,
|
|
108
110
|
}),
|
|
109
111
|
plan,
|
|
@@ -125,155 +127,186 @@ export async function editPlan(input) {
|
|
|
125
127
|
planFile,
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
if (input.patch?.tasks !== undefined && (input.taskId !== undefined || input.taskStatus !== undefined)) {
|
|
131
|
+
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.");
|
|
132
|
+
}
|
|
133
|
+
return withSerializedAccess(planPath, async () => {
|
|
134
|
+
const previous = normalizePlanDocument(readJsonFile(planPath));
|
|
135
|
+
const previousStatus = previous.status;
|
|
136
|
+
const events = [];
|
|
137
|
+
const changedTaskIds = [];
|
|
138
|
+
const appendedTaskIds = [];
|
|
139
|
+
const completedTaskIds = [];
|
|
140
|
+
const next = structuredClone(previous);
|
|
141
|
+
const requestedStatus = input.planStatus ? normalizePlanStatus(input.planStatus) : undefined;
|
|
142
|
+
if (requestedStatus) {
|
|
143
|
+
const validation = canSetPlanStatus(previousStatus, requestedStatus);
|
|
144
|
+
if (!validation.ok) {
|
|
145
|
+
throw new ClawError(validation.code, validation.error);
|
|
146
|
+
}
|
|
147
|
+
next.status = requestedStatus;
|
|
139
148
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (input.patch) {
|
|
143
|
-
applyPlanPatch(next, input.patch);
|
|
144
|
-
}
|
|
145
|
-
if (input.appendTasks?.length) {
|
|
146
|
-
if (isEnd(previous.status) && !requestedStatus && input.patch?.status === undefined) {
|
|
147
|
-
next.status = "prepare.requirements";
|
|
149
|
+
if (input.patch) {
|
|
150
|
+
applyPlanPatch(next, input.patch);
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if (input.appendTasks?.length) {
|
|
153
|
+
if (isEnd(previous.status) && !requestedStatus && input.patch?.status === undefined) {
|
|
154
|
+
next.status = "prepare.requirements";
|
|
155
|
+
}
|
|
156
|
+
const currentIds = new Set(next.tasks.map((taskItem) => taskItem.id));
|
|
157
|
+
const appendedTasks = normalizePlanTasks(input.appendTasks, nextAvailableTaskId(next.tasks));
|
|
158
|
+
for (const taskItem of appendedTasks) {
|
|
159
|
+
if (currentIds.has(taskItem.id)) {
|
|
160
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${taskItem.id} already exists in plan.`);
|
|
161
|
+
}
|
|
162
|
+
validatePlanTask(taskItem);
|
|
163
|
+
next.tasks.push(taskItem);
|
|
164
|
+
currentIds.add(taskItem.id);
|
|
165
|
+
appendedTaskIds.push(taskItem.id);
|
|
154
166
|
}
|
|
155
|
-
validatePlanTask(taskItem);
|
|
156
|
-
next.tasks.push(taskItem);
|
|
157
|
-
currentIds.add(taskItem.id);
|
|
158
167
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (input.taskId
|
|
162
|
-
|
|
168
|
+
next.requirements = normalizePlanRequirements(next.requirements);
|
|
169
|
+
next.tasks = normalizePlanTasks(Array.isArray(next.tasks) ? next.tasks : []);
|
|
170
|
+
if (input.taskId !== undefined || input.taskStatus !== undefined) {
|
|
171
|
+
if (input.taskId === undefined || input.taskStatus === undefined) {
|
|
172
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "taskId and taskStatus must be provided together when updating a plan task status.");
|
|
173
|
+
}
|
|
174
|
+
if (!isProcess(next.status)) {
|
|
175
|
+
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.", {
|
|
176
|
+
planStatus: next.status,
|
|
177
|
+
suggestedCommand: `claw plan edit --task ${task.taskName}${planFile === "plan.json" ? "" : ` --plan ${planFile}`} --plan-status process.active`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const planTask = next.tasks.find((item) => item.id === input.taskId);
|
|
181
|
+
if (!planTask) {
|
|
182
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${input.taskId} was not found in this plan.`);
|
|
183
|
+
}
|
|
184
|
+
validatePlanTaskStatus(input.taskStatus);
|
|
185
|
+
const previousTaskStatus = planTask.status;
|
|
186
|
+
planTask.status = input.taskStatus;
|
|
187
|
+
if (input.taskChoiceId !== undefined) {
|
|
188
|
+
planTask.choiceId = input.taskChoiceId;
|
|
189
|
+
}
|
|
190
|
+
else if (input.taskStatus !== "done") {
|
|
191
|
+
delete planTask.choiceId;
|
|
192
|
+
}
|
|
193
|
+
changedTaskIds.push(planTask.id);
|
|
194
|
+
if (previousTaskStatus !== "done" && input.taskStatus === "done") {
|
|
195
|
+
completedTaskIds.push(planTask.id);
|
|
196
|
+
events.push(buildPlanEvent("plan_task_completed", {
|
|
197
|
+
planPath,
|
|
198
|
+
planTitle: next.title,
|
|
199
|
+
planStatus: next.status,
|
|
200
|
+
taskId: planTask.id,
|
|
201
|
+
affectedPlanTaskIds: [planTask.id],
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
163
204
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
await validateDoneTransitions({
|
|
206
|
+
projectRoot: task.project.projectRoot,
|
|
207
|
+
previousPlan: previous,
|
|
208
|
+
nextPlan: next,
|
|
209
|
+
});
|
|
210
|
+
validatePlanDocument(next);
|
|
211
|
+
if (next.status === "end.completed" && !next.retrospective?.summary?.trim()) {
|
|
212
|
+
throw new ClawError("RETROSPECTIVE_REQUIRED", "end.completed requires retrospective.summary before the plan can be completed.");
|
|
169
213
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
214
|
+
withFileLock(planPath, () => {
|
|
215
|
+
const current = normalizePlanDocument(readJsonFile(planPath));
|
|
216
|
+
if (!plansMatch(current, previous)) {
|
|
217
|
+
throw new ClawError("PLAN_STALE_EDIT", `Plan "${planFile}" changed after this edit command read it. Re-run the edit after inspecting the latest plan state.`, {
|
|
218
|
+
taskName: task.taskName,
|
|
219
|
+
planFile,
|
|
220
|
+
planPath,
|
|
221
|
+
suggestedCommand: `claw plan show --task ${task.taskName}${planFile === "plan.json" ? "" : ` --plan ${planFile}`}`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
writeJsonFile(planPath, next);
|
|
225
|
+
});
|
|
226
|
+
task.meta.activePlan = planFile;
|
|
227
|
+
task.meta.rules = next.rules;
|
|
228
|
+
task.meta.status = taskMetaStatusForPlanStatus(next.status);
|
|
229
|
+
if (next.taskType !== undefined) {
|
|
230
|
+
task.meta.taskType = next.taskType;
|
|
173
231
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
changedTaskIds.push(planTask.id);
|
|
178
|
-
if (previousTaskStatus !== "done" && input.taskStatus === "done") {
|
|
179
|
-
completedTaskIds.push(planTask.id);
|
|
180
|
-
events.push(buildPlanEvent("plan_task_completed", {
|
|
232
|
+
saveTaskMeta(task);
|
|
233
|
+
if (previousStatus !== next.status || input.patch || changedTaskIds.length > 0 || input.appendTasks?.length) {
|
|
234
|
+
events.unshift(buildPlanEvent("plan_changed", {
|
|
181
235
|
planPath,
|
|
182
236
|
planTitle: next.title,
|
|
183
237
|
planStatus: next.status,
|
|
184
|
-
|
|
185
|
-
affectedPlanTaskIds:
|
|
238
|
+
previousStatus,
|
|
239
|
+
...(changedTaskIds.length > 0 ? { affectedPlanTaskIds: changedTaskIds } : {}),
|
|
186
240
|
}));
|
|
187
241
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
task.meta.status = taskMetaStatusForPlanStatus(next.status);
|
|
199
|
-
if (next.taskType !== undefined) {
|
|
200
|
-
task.meta.taskType = next.taskType;
|
|
201
|
-
}
|
|
202
|
-
saveTaskMeta(task);
|
|
203
|
-
if (previousStatus !== next.status || input.patch || changedTaskIds.length > 0 || input.appendTasks?.length) {
|
|
204
|
-
events.unshift(buildPlanEvent("plan_changed", {
|
|
205
|
-
planPath,
|
|
206
|
-
planTitle: next.title,
|
|
207
|
-
planStatus: next.status,
|
|
208
|
-
previousStatus,
|
|
209
|
-
...(changedTaskIds.length > 0 ? { affectedPlanTaskIds: changedTaskIds } : {}),
|
|
210
|
-
}));
|
|
211
|
-
}
|
|
212
|
-
const completionHooks = previousStatus !== "end.completed" && next.status === "end.completed"
|
|
213
|
-
? buildCompletionHooks({ task, planPath, plan: next })
|
|
214
|
-
: undefined;
|
|
215
|
-
if (completionHooks) {
|
|
216
|
-
events.push(buildPlanEvent("plan_completed", {
|
|
217
|
-
planPath,
|
|
218
|
-
planTitle: next.title,
|
|
219
|
-
planStatus: next.status,
|
|
220
|
-
previousStatus,
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
|
-
let resultPlanFile = planFile;
|
|
224
|
-
let resultPlanPath = planPath;
|
|
225
|
-
let resultPlan = next;
|
|
226
|
-
if (completionHooks?.subplanClosureCandidate) {
|
|
227
|
-
const resumedParent = completeSubplanAndResumeParent({
|
|
228
|
-
task,
|
|
229
|
-
closure: completionHooks.subplanClosureCandidate,
|
|
230
|
-
});
|
|
231
|
-
resultPlanFile = resumedParent.planFile;
|
|
232
|
-
resultPlanPath = resumedParent.planPath;
|
|
233
|
-
resultPlan = resumedParent.plan;
|
|
234
|
-
task.meta.activePlan = resumedParent.planFile;
|
|
235
|
-
task.meta.rules = resumedParent.plan.rules;
|
|
236
|
-
task.meta.status = taskMetaStatusForPlanStatus(resumedParent.plan.status);
|
|
237
|
-
if (resumedParent.plan.taskType !== undefined) {
|
|
238
|
-
task.meta.taskType = resumedParent.plan.taskType;
|
|
242
|
+
const completionHooks = previousStatus !== "end.completed" && next.status === "end.completed"
|
|
243
|
+
? buildCompletionHooks({ task, planPath, plan: next })
|
|
244
|
+
: undefined;
|
|
245
|
+
if (completionHooks) {
|
|
246
|
+
events.push(buildPlanEvent("plan_completed", {
|
|
247
|
+
planPath,
|
|
248
|
+
planTitle: next.title,
|
|
249
|
+
planStatus: next.status,
|
|
250
|
+
previousStatus,
|
|
251
|
+
}));
|
|
239
252
|
}
|
|
240
|
-
|
|
241
|
-
|
|
253
|
+
let resultPlanFile = planFile;
|
|
254
|
+
let resultPlanPath = planPath;
|
|
255
|
+
let resultPlan = next;
|
|
256
|
+
if (completionHooks?.subplanClosureCandidate) {
|
|
257
|
+
const resumedParent = completeSubplanAndResumeParent({
|
|
258
|
+
task,
|
|
259
|
+
closure: completionHooks.subplanClosureCandidate,
|
|
260
|
+
});
|
|
261
|
+
resultPlanFile = resumedParent.planFile;
|
|
262
|
+
resultPlanPath = resumedParent.planPath;
|
|
263
|
+
resultPlan = resumedParent.plan;
|
|
264
|
+
task.meta.activePlan = resumedParent.planFile;
|
|
265
|
+
task.meta.rules = resumedParent.plan.rules;
|
|
266
|
+
task.meta.status = taskMetaStatusForPlanStatus(resumedParent.plan.status);
|
|
267
|
+
if (resumedParent.plan.taskType !== undefined) {
|
|
268
|
+
task.meta.taskType = resumedParent.plan.taskType;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
delete task.meta.taskType;
|
|
272
|
+
}
|
|
273
|
+
saveTaskMeta(task);
|
|
242
274
|
}
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
taskName: task.taskName,
|
|
247
|
-
planPath: resultPlanPath,
|
|
248
|
-
planFile: resultPlanFile,
|
|
249
|
-
planStatus: resultPlan.status,
|
|
250
|
-
previousPlanStatus: previousStatus,
|
|
251
|
-
emittedEvents: events.map((event) => event.type),
|
|
252
|
-
changedTaskIds,
|
|
253
|
-
...(completionHooks ? { completionHooks } : {}),
|
|
254
|
-
workflowGuidance: buildPlanWorkflowGuidance({
|
|
255
|
-
taskName: task.taskName,
|
|
256
|
-
planFile: resultPlanFile,
|
|
257
|
-
plan: resultPlan,
|
|
258
|
-
commandSource: "plan.edit",
|
|
259
|
-
projectConfig: task.project.projectConfig,
|
|
260
|
-
host: input.host,
|
|
261
|
-
...(completionHooks?.subplanClosureCandidate
|
|
262
|
-
? {}
|
|
263
|
-
: {
|
|
264
|
-
previousStatus,
|
|
265
|
-
completionHooks,
|
|
266
|
-
changedTaskIds,
|
|
267
|
-
completedTaskIds,
|
|
268
|
-
}),
|
|
269
|
-
}),
|
|
270
|
-
planView: buildPlanViewModel({
|
|
275
|
+
return {
|
|
271
276
|
taskName: task.taskName,
|
|
277
|
+
planPath: resultPlanPath,
|
|
272
278
|
planFile: resultPlanFile,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
279
|
+
planStatus: resultPlan.status,
|
|
280
|
+
previousPlanStatus: previousStatus,
|
|
281
|
+
emittedEvents: events.map((event) => event.type),
|
|
282
|
+
changedTaskIds,
|
|
283
|
+
appendedTaskIds,
|
|
284
|
+
...(completionHooks ? { completionHooks } : {}),
|
|
285
|
+
workflowGuidance: await buildPlanWorkflowGuidance({
|
|
286
|
+
taskName: task.taskName,
|
|
287
|
+
planFile: resultPlanFile,
|
|
288
|
+
plan: resultPlan,
|
|
289
|
+
commandSource: "plan.edit",
|
|
290
|
+
projectRoot: task.project.projectRoot,
|
|
291
|
+
projectConfig: resolvePlanEffectiveConfig(task.project.projectConfig, resultPlan),
|
|
292
|
+
host: input.host,
|
|
293
|
+
...(completionHooks?.subplanClosureCandidate
|
|
294
|
+
? {}
|
|
295
|
+
: {
|
|
296
|
+
previousStatus,
|
|
297
|
+
completionHooks,
|
|
298
|
+
changedTaskIds,
|
|
299
|
+
completedTaskIds,
|
|
300
|
+
}),
|
|
301
|
+
}),
|
|
302
|
+
planView: buildPlanViewModel({
|
|
303
|
+
taskName: task.taskName,
|
|
304
|
+
planFile: resultPlanFile,
|
|
305
|
+
plan: resultPlan,
|
|
306
|
+
}),
|
|
307
|
+
events,
|
|
308
|
+
};
|
|
309
|
+
});
|
|
277
310
|
}
|
|
278
311
|
export function showPlan(input) {
|
|
279
312
|
const resolved = resolveShowPlanTarget(input);
|
|
@@ -357,6 +390,9 @@ function normalizePlanRequirements(requirements) {
|
|
|
357
390
|
acceptanceCriteria: Array.isArray(requirements?.acceptanceCriteria) ? requirements.acceptanceCriteria : [],
|
|
358
391
|
};
|
|
359
392
|
}
|
|
393
|
+
function plansMatch(left, right) {
|
|
394
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
395
|
+
}
|
|
360
396
|
function normalizePlanTasks(tasks, startId = 1) {
|
|
361
397
|
const explicitIds = tasks
|
|
362
398
|
.map((task) => task.id)
|
|
@@ -387,6 +423,7 @@ function normalizePlanTask(task, fallbackId) {
|
|
|
387
423
|
const normalizedTask = {
|
|
388
424
|
...task,
|
|
389
425
|
id: Number.isInteger(task.id) ? task.id : fallbackId ?? task.id,
|
|
426
|
+
status: task.status ?? "pending",
|
|
390
427
|
};
|
|
391
428
|
validatePlanTask(normalizedTask);
|
|
392
429
|
return normalizedTask;
|
|
@@ -416,59 +453,55 @@ function validatePlanTask(task) {
|
|
|
416
453
|
throw new ClawError("PROJECT_CONFIG_INVALID", `Plan task ${task.id} is missing title.`);
|
|
417
454
|
}
|
|
418
455
|
validatePlanTaskStatus(task.status);
|
|
456
|
+
if (task.choiceId !== undefined) {
|
|
457
|
+
if (typeof task.choiceId !== "string" || !task.choiceId.trim()) {
|
|
458
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Plan task ${task.id} has an invalid choiceId.`);
|
|
459
|
+
}
|
|
460
|
+
if (task.status !== "done") {
|
|
461
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Plan task ${task.id} can only use choiceId when status is done.`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
419
464
|
}
|
|
420
465
|
function validatePlanTaskStatus(status) {
|
|
421
466
|
if (!PLAN_TASK_STATUSES.includes(status)) {
|
|
422
467
|
throw new ClawError("PROJECT_CONFIG_INVALID", `Unsupported plan task status "${status}". Canonical values are pending, in_progress, subagent_running, done, blocked.`);
|
|
423
468
|
}
|
|
424
469
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
target.parentTaskId = patch.parentTaskId;
|
|
449
|
-
}
|
|
450
|
-
if (patch.references !== undefined) {
|
|
451
|
-
target.references = patch.references;
|
|
452
|
-
}
|
|
453
|
-
if (patch.rules !== undefined) {
|
|
454
|
-
target.rules = patch.rules;
|
|
455
|
-
}
|
|
456
|
-
if (patch.retrospective !== undefined) {
|
|
457
|
-
target.retrospective = patch.retrospective;
|
|
458
|
-
}
|
|
459
|
-
if (patch.keyDecisions !== undefined) {
|
|
460
|
-
target.keyDecisions = patch.keyDecisions;
|
|
470
|
+
const DELETE_PATCH_VALUE = Symbol("delete-plan-patch-value");
|
|
471
|
+
function isPlainPatchObject(value) {
|
|
472
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
473
|
+
}
|
|
474
|
+
function mergePlanPatchValue(current, patch) {
|
|
475
|
+
if (patch === null) {
|
|
476
|
+
return DELETE_PATCH_VALUE;
|
|
477
|
+
}
|
|
478
|
+
if (Array.isArray(patch)) {
|
|
479
|
+
return structuredClone(patch);
|
|
480
|
+
}
|
|
481
|
+
if (isPlainPatchObject(patch)) {
|
|
482
|
+
const base = isPlainPatchObject(current) ? structuredClone(current) : {};
|
|
483
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
484
|
+
const merged = mergePlanPatchValue(base[key], value);
|
|
485
|
+
if (merged === DELETE_PATCH_VALUE) {
|
|
486
|
+
delete base[key];
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
base[key] = merged;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return base;
|
|
461
493
|
}
|
|
462
|
-
|
|
463
|
-
|
|
494
|
+
return patch;
|
|
495
|
+
}
|
|
496
|
+
function applyPlanPatch(target, patch) {
|
|
497
|
+
const merged = mergePlanPatchValue(target, patch);
|
|
498
|
+
if (!isPlainPatchObject(merged)) {
|
|
499
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "Plan patch must be a JSON object.");
|
|
464
500
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (!normalized) {
|
|
468
|
-
throw new ClawError("PLAN_STATUS_INVALID", `Unsupported plan status "${String(patch.status)}".`);
|
|
469
|
-
}
|
|
470
|
-
target.status = normalized;
|
|
501
|
+
for (const key of Object.keys(target)) {
|
|
502
|
+
delete target[key];
|
|
471
503
|
}
|
|
504
|
+
Object.assign(target, merged);
|
|
472
505
|
}
|
|
473
506
|
async function createSeedPlan(projectRoot, projectConfig, templateName, taskName, title, goalText, status = "prepare.requirements", forcePlanning = false, host) {
|
|
474
507
|
const effectiveTemplateName = templateName?.trim() || projectConfig?.defaultPlanTemplate?.trim() || defaultPlanTemplateName();
|
|
@@ -476,12 +509,17 @@ async function createSeedPlan(projectRoot, projectConfig, templateName, taskName
|
|
|
476
509
|
projectRoot,
|
|
477
510
|
templateName: effectiveTemplateName,
|
|
478
511
|
});
|
|
479
|
-
const
|
|
480
|
-
|
|
512
|
+
const effectiveConfig = resolvePlanEffectiveConfig(projectConfig, {
|
|
513
|
+
configOverride: template.configOverride,
|
|
514
|
+
});
|
|
515
|
+
const planningEnabled = forcePlanning || effectiveConfig?.planning !== false;
|
|
516
|
+
const planningSkill = effectiveConfig?.externalPlanningSkill?.trim() || "the built-in planning skill";
|
|
481
517
|
if (!planningEnabled) {
|
|
482
518
|
return {
|
|
483
519
|
title: title ?? taskName,
|
|
484
|
-
|
|
520
|
+
templateId: template.id,
|
|
521
|
+
...(template.configOverride ? { configOverride: template.configOverride } : {}),
|
|
522
|
+
status: "process.active",
|
|
485
523
|
goal: {
|
|
486
524
|
text: goalText ?? title ?? taskName,
|
|
487
525
|
},
|
|
@@ -505,47 +543,84 @@ async function createSeedPlan(projectRoot, projectConfig, templateName, taskName
|
|
|
505
543
|
},
|
|
506
544
|
};
|
|
507
545
|
}
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
546
|
+
const effectiveTitle = title ?? template.title ?? taskName;
|
|
547
|
+
const effectiveGoalText = goalText ?? (template.goal?.text?.trim() || effectiveTitle);
|
|
548
|
+
const compiledTasks = template.tasks.map((taskItem) => ({
|
|
549
|
+
id: taskItem.id,
|
|
550
|
+
title: taskItem.title,
|
|
551
|
+
...(taskItem.detail || taskItem.goalModeDetail
|
|
552
|
+
? {
|
|
553
|
+
detail: buildCompiledTaskDetail({
|
|
554
|
+
baseDetail: taskItem.detail,
|
|
555
|
+
goalModeDetail: taskItem.goalModeDetail,
|
|
556
|
+
host,
|
|
557
|
+
goalModeEnabled: effectiveConfig?.goalMode !== false,
|
|
558
|
+
planGoal: effectiveGoalText,
|
|
559
|
+
planningSkill,
|
|
560
|
+
}),
|
|
561
|
+
}
|
|
562
|
+
: {}),
|
|
563
|
+
status: taskItem.status,
|
|
564
|
+
...(taskItem.execution ? { execution: taskItem.execution } : {}),
|
|
565
|
+
...(taskItem.sessionKey ? { sessionKey: taskItem.sessionKey } : {}),
|
|
566
|
+
}));
|
|
516
567
|
return {
|
|
517
|
-
title:
|
|
518
|
-
|
|
568
|
+
title: effectiveTitle,
|
|
569
|
+
templateId: template.id,
|
|
570
|
+
...(template.configOverride ? { configOverride: template.configOverride } : {}),
|
|
571
|
+
status: template.status ?? status,
|
|
519
572
|
goal: {
|
|
520
573
|
text: effectiveGoalText,
|
|
521
574
|
},
|
|
522
|
-
requirements: {
|
|
575
|
+
requirements: template.requirements ?? {
|
|
523
576
|
summary: "",
|
|
524
577
|
openQuestions: [],
|
|
525
578
|
acceptanceCriteria: [],
|
|
526
579
|
},
|
|
527
|
-
tasks:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
status: "pending",
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
id: 2,
|
|
536
|
-
title: template.activationTask.title,
|
|
537
|
-
detail: activationDetail,
|
|
538
|
-
status: "pending",
|
|
539
|
-
},
|
|
540
|
-
],
|
|
541
|
-
references: [],
|
|
542
|
-
rules: [],
|
|
543
|
-
keyDecisions: [],
|
|
544
|
-
retrospective: {
|
|
580
|
+
tasks: compiledTasks,
|
|
581
|
+
references: template.references ?? [],
|
|
582
|
+
rules: template.rules ?? [],
|
|
583
|
+
keyDecisions: template.keyDecisions ?? [],
|
|
584
|
+
retrospective: template.retrospective ?? {
|
|
545
585
|
summary: "",
|
|
546
586
|
},
|
|
547
587
|
};
|
|
548
588
|
}
|
|
589
|
+
async function validateDoneTransitions(params) {
|
|
590
|
+
const { projectRoot, previousPlan, nextPlan } = params;
|
|
591
|
+
if (!nextPlan.templateId?.trim()) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const template = await resolveSeedPlanTemplate({
|
|
595
|
+
projectRoot,
|
|
596
|
+
templateName: nextPlan.templateId,
|
|
597
|
+
});
|
|
598
|
+
const previousTasks = new Map(previousPlan.tasks.map((task) => [task.id, task]));
|
|
599
|
+
for (const task of nextPlan.tasks) {
|
|
600
|
+
const previousTask = previousTasks.get(task.id);
|
|
601
|
+
const choices = getTemplateTaskDoneChoices(template, task.id);
|
|
602
|
+
const availableChoices = choices ? Object.keys(choices) : [];
|
|
603
|
+
const isDoneTransition = previousTask?.status !== "done" && task.status === "done";
|
|
604
|
+
if (isDoneTransition && choices && !task.choiceId) {
|
|
605
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task ${task.id} requires choiceId because this template defines onDone choices. Provide one of: ${availableChoices.join(", ")}.`, {
|
|
606
|
+
taskId: task.id,
|
|
607
|
+
availableChoices,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (task.choiceId && !choices) {
|
|
611
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task ${task.id} does not define onDone choices, so choiceId is not allowed.`, {
|
|
612
|
+
taskId: task.id,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (task.choiceId && choices && !Object.prototype.hasOwnProperty.call(choices, task.choiceId)) {
|
|
616
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task ${task.id} has an invalid choiceId "${task.choiceId}". Expected one of: ${availableChoices.join(", ")}.`, {
|
|
617
|
+
taskId: task.id,
|
|
618
|
+
choiceId: task.choiceId,
|
|
619
|
+
availableChoices,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
549
624
|
function defaultPlanTemplateName() {
|
|
550
625
|
return "default";
|
|
551
626
|
}
|
|
@@ -562,6 +637,19 @@ function buildActivationTaskDetail(params) {
|
|
|
562
637
|
: goalModeDetail;
|
|
563
638
|
return `${baseDetail} ${normalizedGoalModeDetail} and use \`${buildGoalModeObjective(planGoal)}\` as the goal objective.`;
|
|
564
639
|
}
|
|
640
|
+
function buildCompiledTaskDetail(params) {
|
|
641
|
+
const renderedBase = params.baseDetail ? renderSeedTemplateText(params.baseDetail, { planningSkill: params.planningSkill }) : "";
|
|
642
|
+
if (!params.goalModeDetail) {
|
|
643
|
+
return renderedBase || undefined;
|
|
644
|
+
}
|
|
645
|
+
return buildActivationTaskDetail({
|
|
646
|
+
baseDetail: renderedBase,
|
|
647
|
+
goalModeDetail: params.goalModeDetail,
|
|
648
|
+
host: params.host,
|
|
649
|
+
goalModeEnabled: params.goalModeEnabled,
|
|
650
|
+
planGoal: params.planGoal,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
565
653
|
function deriveTaskName(input) {
|
|
566
654
|
if (input.taskName?.trim()) {
|
|
567
655
|
return resolveTaskName(input.taskName);
|