@veewo/claw-core 0.1.59 → 0.1.61
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 +0 -1
- package/dist/src/context.js.map +1 -1
- 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 +0 -1
- package/dist/src/index.js +0 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/init.js +0 -1
- package/dist/src/init.js.map +1 -1
- package/dist/src/io.d.ts +0 -4
- package/dist/src/io.js +0 -55
- package/dist/src/io.js.map +1 -1
- package/dist/src/paths.d.ts +0 -1
- package/dist/src/paths.js +1 -25
- package/dist/src/paths.js.map +1 -1
- package/dist/src/plan-templates.d.ts +2 -25
- package/dist/src/plan-templates.js +28 -582
- package/dist/src/plan-templates.js.map +1 -1
- package/dist/src/plan.js +213 -301
- package/dist/src/plan.js.map +1 -1
- package/dist/src/project-check.js +0 -10
- 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 +14 -44
- package/dist/src/templates/plans/default.js +10 -44
- package/dist/src/templates/plans/default.js.map +1 -1
- package/dist/src/types.d.ts +1 -15
- package/dist/src/workflow-guidance.config.json +366 -367
- package/dist/src/workflow-guidance.d.ts +1 -2
- package/dist/src/workflow-guidance.js +21 -166
- package/dist/src/workflow-guidance.js.map +1 -1
- package/package.json +44 -44
- package/dist/src/effective-config.d.ts +0 -2
- package/dist/src/effective-config.js +0 -10
- package/dist/src/effective-config.js.map +0 -1
package/dist/src/plan.js
CHANGED
|
@@ -2,13 +2,12 @@ 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";
|
|
6
5
|
import { ClawError } from "./errors.js";
|
|
7
|
-
import { readJsonFile, withFileLock,
|
|
6
|
+
import { readJsonFile, withFileLock, writeJsonFile } from "./io.js";
|
|
8
7
|
import { buildPlanEvent } from "./plan-events.js";
|
|
9
8
|
import { isProcessStatus, } from "./requirements-gate.js";
|
|
10
9
|
import { buildPlanViewModel } from "./plan-view.js";
|
|
11
|
-
import {
|
|
10
|
+
import { renderSeedTemplateText, resolveSeedPlanTemplate } from "./plan-templates.js";
|
|
12
11
|
import { ensureInsideDir, normalizePlanFile, slugFromFilePath } from "./paths.js";
|
|
13
12
|
import { buildGoalModeObjective, buildPlanWorkflowGuidance } from "./workflow-guidance.js";
|
|
14
13
|
const PLAN_STATUSES = [
|
|
@@ -99,13 +98,12 @@ export async function writePlan(input) {
|
|
|
99
98
|
eventType,
|
|
100
99
|
...(plan.parentPlan ? { parentPlan: plan.parentPlan } : {}),
|
|
101
100
|
...(plan.parentTaskId !== undefined ? { parentTaskId: plan.parentTaskId } : {}),
|
|
102
|
-
workflowGuidance:
|
|
101
|
+
workflowGuidance: buildPlanWorkflowGuidance({
|
|
103
102
|
taskName,
|
|
104
103
|
planFile,
|
|
105
104
|
plan,
|
|
106
105
|
commandSource: input.parentTaskId !== undefined ? "subplan.create" : "plan.create",
|
|
107
|
-
|
|
108
|
-
projectConfig: resolvePlanEffectiveConfig(project.projectConfig, plan),
|
|
106
|
+
projectConfig: project.projectConfig,
|
|
109
107
|
host: input.host,
|
|
110
108
|
}),
|
|
111
109
|
plan,
|
|
@@ -127,186 +125,155 @@ export async function editPlan(input) {
|
|
|
127
125
|
planFile,
|
|
128
126
|
});
|
|
129
127
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
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;
|
|
128
|
+
const previous = normalizePlanDocument(readJsonFile(planPath));
|
|
129
|
+
const previousStatus = previous.status;
|
|
130
|
+
const events = [];
|
|
131
|
+
const changedTaskIds = [];
|
|
132
|
+
const completedTaskIds = [];
|
|
133
|
+
const next = structuredClone(previous);
|
|
134
|
+
const requestedStatus = input.planStatus ? normalizePlanStatus(input.planStatus) : undefined;
|
|
135
|
+
if (requestedStatus) {
|
|
136
|
+
const validation = canSetPlanStatus(previousStatus, requestedStatus);
|
|
137
|
+
if (!validation.ok) {
|
|
138
|
+
throw new ClawError(validation.code, validation.error);
|
|
148
139
|
}
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
next.status = requestedStatus;
|
|
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";
|
|
151
148
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
149
|
+
const currentIds = new Set(next.tasks.map((taskItem) => taskItem.id));
|
|
150
|
+
const appendedTasks = normalizePlanTasks(input.appendTasks, nextAvailableTaskId(next.tasks));
|
|
151
|
+
for (const taskItem of appendedTasks) {
|
|
152
|
+
if (currentIds.has(taskItem.id)) {
|
|
153
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${taskItem.id} already exists in plan.`);
|
|
166
154
|
}
|
|
155
|
+
validatePlanTask(taskItem);
|
|
156
|
+
next.tasks.push(taskItem);
|
|
157
|
+
currentIds.add(taskItem.id);
|
|
167
158
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (input.taskId
|
|
171
|
-
|
|
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
|
-
}
|
|
159
|
+
}
|
|
160
|
+
if (input.taskId !== undefined || input.taskStatus !== undefined) {
|
|
161
|
+
if (input.taskId === undefined || input.taskStatus === undefined) {
|
|
162
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", "taskId and taskStatus must be provided together when updating a plan task status.");
|
|
204
163
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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.");
|
|
164
|
+
if (!isProcess(next.status)) {
|
|
165
|
+
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.", {
|
|
166
|
+
planStatus: next.status,
|
|
167
|
+
suggestedCommand: `claw plan edit --task ${task.taskName}${planFile === "plan.json" ? "" : ` --plan ${planFile}`} --plan-status process.active`,
|
|
168
|
+
});
|
|
213
169
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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;
|
|
170
|
+
const planTask = next.tasks.find((item) => item.id === input.taskId);
|
|
171
|
+
if (!planTask) {
|
|
172
|
+
throw new ClawError("PROJECT_CONFIG_INVALID", `Task id ${input.taskId} was not found in this plan.`);
|
|
231
173
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
174
|
+
validatePlanTaskStatus(input.taskStatus);
|
|
175
|
+
const previousTaskStatus = planTask.status;
|
|
176
|
+
planTask.status = input.taskStatus;
|
|
177
|
+
changedTaskIds.push(planTask.id);
|
|
178
|
+
if (previousTaskStatus !== "done" && input.taskStatus === "done") {
|
|
179
|
+
completedTaskIds.push(planTask.id);
|
|
180
|
+
events.push(buildPlanEvent("plan_task_completed", {
|
|
235
181
|
planPath,
|
|
236
182
|
planTitle: next.title,
|
|
237
183
|
planStatus: next.status,
|
|
238
|
-
|
|
239
|
-
|
|
184
|
+
taskId: planTask.id,
|
|
185
|
+
affectedPlanTaskIds: [planTask.id],
|
|
240
186
|
}));
|
|
241
187
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
188
|
+
}
|
|
189
|
+
validatePlanDocument(next);
|
|
190
|
+
if (next.status === "end.completed" && !next.retrospective?.summary?.trim()) {
|
|
191
|
+
throw new ClawError("RETROSPECTIVE_REQUIRED", "end.completed requires retrospective.summary before the plan can be completed.");
|
|
192
|
+
}
|
|
193
|
+
withFileLock(planPath, () => {
|
|
194
|
+
writeJsonFile(planPath, next);
|
|
195
|
+
});
|
|
196
|
+
task.meta.activePlan = planFile;
|
|
197
|
+
task.meta.rules = next.rules;
|
|
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;
|
|
252
239
|
}
|
|
253
|
-
|
|
254
|
-
|
|
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);
|
|
240
|
+
else {
|
|
241
|
+
delete task.meta.taskType;
|
|
274
242
|
}
|
|
275
|
-
|
|
243
|
+
saveTaskMeta(task);
|
|
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({
|
|
276
255
|
taskName: task.taskName,
|
|
277
|
-
planPath: resultPlanPath,
|
|
278
256
|
planFile: resultPlanFile,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
completedTaskIds,
|
|
300
|
-
}),
|
|
301
|
-
}),
|
|
302
|
-
planView: buildPlanViewModel({
|
|
303
|
-
taskName: task.taskName,
|
|
304
|
-
planFile: resultPlanFile,
|
|
305
|
-
plan: resultPlan,
|
|
306
|
-
}),
|
|
307
|
-
events,
|
|
308
|
-
};
|
|
309
|
-
});
|
|
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({
|
|
271
|
+
taskName: task.taskName,
|
|
272
|
+
planFile: resultPlanFile,
|
|
273
|
+
plan: resultPlan,
|
|
274
|
+
}),
|
|
275
|
+
events,
|
|
276
|
+
};
|
|
310
277
|
}
|
|
311
278
|
export function showPlan(input) {
|
|
312
279
|
const resolved = resolveShowPlanTarget(input);
|
|
@@ -390,9 +357,6 @@ function normalizePlanRequirements(requirements) {
|
|
|
390
357
|
acceptanceCriteria: Array.isArray(requirements?.acceptanceCriteria) ? requirements.acceptanceCriteria : [],
|
|
391
358
|
};
|
|
392
359
|
}
|
|
393
|
-
function plansMatch(left, right) {
|
|
394
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
395
|
-
}
|
|
396
360
|
function normalizePlanTasks(tasks, startId = 1) {
|
|
397
361
|
const explicitIds = tasks
|
|
398
362
|
.map((task) => task.id)
|
|
@@ -423,7 +387,6 @@ function normalizePlanTask(task, fallbackId) {
|
|
|
423
387
|
const normalizedTask = {
|
|
424
388
|
...task,
|
|
425
389
|
id: Number.isInteger(task.id) ? task.id : fallbackId ?? task.id,
|
|
426
|
-
status: task.status ?? "pending",
|
|
427
390
|
};
|
|
428
391
|
validatePlanTask(normalizedTask);
|
|
429
392
|
return normalizedTask;
|
|
@@ -453,55 +416,59 @@ function validatePlanTask(task) {
|
|
|
453
416
|
throw new ClawError("PROJECT_CONFIG_INVALID", `Plan task ${task.id} is missing title.`);
|
|
454
417
|
}
|
|
455
418
|
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
|
-
}
|
|
464
419
|
}
|
|
465
420
|
function validatePlanTaskStatus(status) {
|
|
466
421
|
if (!PLAN_TASK_STATUSES.includes(status)) {
|
|
467
422
|
throw new ClawError("PROJECT_CONFIG_INVALID", `Unsupported plan task status "${status}". Canonical values are pending, in_progress, subagent_running, done, blocked.`);
|
|
468
423
|
}
|
|
469
424
|
}
|
|
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;
|
|
493
|
-
}
|
|
494
|
-
return patch;
|
|
495
|
-
}
|
|
496
425
|
function applyPlanPatch(target, patch) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
426
|
+
if (patch.title !== undefined) {
|
|
427
|
+
target.title = patch.title;
|
|
428
|
+
}
|
|
429
|
+
if (patch.goal !== undefined) {
|
|
430
|
+
target.goal = { text: patch.goal.text };
|
|
431
|
+
}
|
|
432
|
+
if (patch.summary !== undefined) {
|
|
433
|
+
target.summary = patch.summary;
|
|
434
|
+
}
|
|
435
|
+
if (patch.requirements !== undefined) {
|
|
436
|
+
target.requirements = normalizePlanRequirements(patch.requirements);
|
|
437
|
+
}
|
|
438
|
+
if (patch.taskType !== undefined) {
|
|
439
|
+
target.taskType = patch.taskType;
|
|
440
|
+
}
|
|
441
|
+
if (patch.leaveReason !== undefined) {
|
|
442
|
+
target.leaveReason = patch.leaveReason;
|
|
443
|
+
}
|
|
444
|
+
if (patch.parentPlan !== undefined) {
|
|
445
|
+
target.parentPlan = patch.parentPlan;
|
|
446
|
+
}
|
|
447
|
+
if (patch.parentTaskId !== undefined) {
|
|
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;
|
|
500
458
|
}
|
|
501
|
-
|
|
502
|
-
|
|
459
|
+
if (patch.keyDecisions !== undefined) {
|
|
460
|
+
target.keyDecisions = patch.keyDecisions;
|
|
461
|
+
}
|
|
462
|
+
if (patch.tasks !== undefined) {
|
|
463
|
+
target.tasks = normalizePlanTasks(patch.tasks);
|
|
464
|
+
}
|
|
465
|
+
if (patch.status !== undefined) {
|
|
466
|
+
const normalized = normalizePlanStatus(patch.status);
|
|
467
|
+
if (!normalized) {
|
|
468
|
+
throw new ClawError("PLAN_STATUS_INVALID", `Unsupported plan status "${String(patch.status)}".`);
|
|
469
|
+
}
|
|
470
|
+
target.status = normalized;
|
|
503
471
|
}
|
|
504
|
-
Object.assign(target, merged);
|
|
505
472
|
}
|
|
506
473
|
async function createSeedPlan(projectRoot, projectConfig, templateName, taskName, title, goalText, status = "prepare.requirements", forcePlanning = false, host) {
|
|
507
474
|
const effectiveTemplateName = templateName?.trim() || projectConfig?.defaultPlanTemplate?.trim() || defaultPlanTemplateName();
|
|
@@ -509,17 +476,12 @@ async function createSeedPlan(projectRoot, projectConfig, templateName, taskName
|
|
|
509
476
|
projectRoot,
|
|
510
477
|
templateName: effectiveTemplateName,
|
|
511
478
|
});
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
});
|
|
515
|
-
const planningEnabled = forcePlanning || effectiveConfig?.planning !== false;
|
|
516
|
-
const planningSkill = effectiveConfig?.externalPlanningSkill?.trim() || "the built-in planning skill";
|
|
479
|
+
const planningEnabled = forcePlanning || projectConfig?.planning !== false;
|
|
480
|
+
const planningSkill = projectConfig?.externalPlanningSkill?.trim() || "the built-in planning skill";
|
|
517
481
|
if (!planningEnabled) {
|
|
518
482
|
return {
|
|
519
483
|
title: title ?? taskName,
|
|
520
|
-
|
|
521
|
-
...(template.configOverride ? { configOverride: template.configOverride } : {}),
|
|
522
|
-
status: "process.active",
|
|
484
|
+
status: template.planningDisabledStatus,
|
|
523
485
|
goal: {
|
|
524
486
|
text: goalText ?? title ?? taskName,
|
|
525
487
|
},
|
|
@@ -543,84 +505,47 @@ async function createSeedPlan(projectRoot, projectConfig, templateName, taskName
|
|
|
543
505
|
},
|
|
544
506
|
};
|
|
545
507
|
}
|
|
546
|
-
const
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
}));
|
|
508
|
+
const effectiveGoalText = goalText ?? title ?? taskName;
|
|
509
|
+
const activationDetail = buildActivationTaskDetail({
|
|
510
|
+
baseDetail: template.activationTask.detail,
|
|
511
|
+
goalModeDetail: template.activationTask.goalModeDetail,
|
|
512
|
+
host,
|
|
513
|
+
goalModeEnabled: projectConfig?.goalMode !== false,
|
|
514
|
+
planGoal: effectiveGoalText,
|
|
515
|
+
});
|
|
567
516
|
return {
|
|
568
|
-
title:
|
|
569
|
-
|
|
570
|
-
...(template.configOverride ? { configOverride: template.configOverride } : {}),
|
|
571
|
-
status: template.status ?? status,
|
|
517
|
+
title: title ?? taskName,
|
|
518
|
+
status: template.planningEnabledStatus ?? status,
|
|
572
519
|
goal: {
|
|
573
520
|
text: effectiveGoalText,
|
|
574
521
|
},
|
|
575
|
-
requirements:
|
|
522
|
+
requirements: {
|
|
576
523
|
summary: "",
|
|
577
524
|
openQuestions: [],
|
|
578
525
|
acceptanceCriteria: [],
|
|
579
526
|
},
|
|
580
|
-
tasks:
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
527
|
+
tasks: [
|
|
528
|
+
{
|
|
529
|
+
id: 1,
|
|
530
|
+
title: template.planningTask.title,
|
|
531
|
+
detail: renderSeedTemplateText(template.planningTask.detail, { planningSkill }),
|
|
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: {
|
|
585
545
|
summary: "",
|
|
586
546
|
},
|
|
587
547
|
};
|
|
588
548
|
}
|
|
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
|
-
}
|
|
624
549
|
function defaultPlanTemplateName() {
|
|
625
550
|
return "default";
|
|
626
551
|
}
|
|
@@ -637,19 +562,6 @@ function buildActivationTaskDetail(params) {
|
|
|
637
562
|
: goalModeDetail;
|
|
638
563
|
return `${baseDetail} ${normalizedGoalModeDetail} and use \`${buildGoalModeObjective(planGoal)}\` as the goal objective.`;
|
|
639
564
|
}
|
|
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
|
-
}
|
|
653
565
|
function deriveTaskName(input) {
|
|
654
566
|
if (input.taskName?.trim()) {
|
|
655
567
|
return resolveTaskName(input.taskName);
|