@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.
Files changed (41) hide show
  1. package/README.md +16 -16
  2. package/dist/src/context.js +0 -1
  3. package/dist/src/context.js.map +1 -1
  4. package/dist/src/embedding-config.d.ts +9 -0
  5. package/dist/src/embedding-config.js +21 -0
  6. package/dist/src/embedding-config.js.map +1 -0
  7. package/dist/src/errors.d.ts +1 -1
  8. package/dist/src/errors.js.map +1 -1
  9. package/dist/src/index.d.ts +0 -1
  10. package/dist/src/index.js +0 -1
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/init.js +0 -1
  13. package/dist/src/init.js.map +1 -1
  14. package/dist/src/io.d.ts +0 -4
  15. package/dist/src/io.js +0 -55
  16. package/dist/src/io.js.map +1 -1
  17. package/dist/src/paths.d.ts +0 -1
  18. package/dist/src/paths.js +1 -25
  19. package/dist/src/paths.js.map +1 -1
  20. package/dist/src/plan-templates.d.ts +2 -25
  21. package/dist/src/plan-templates.js +28 -582
  22. package/dist/src/plan-templates.js.map +1 -1
  23. package/dist/src/plan.js +213 -301
  24. package/dist/src/plan.js.map +1 -1
  25. package/dist/src/project-check.js +0 -10
  26. package/dist/src/project-check.js.map +1 -1
  27. package/dist/src/project-config-defaults.d.ts +6 -0
  28. package/dist/src/project-config-defaults.js +5 -0
  29. package/dist/src/project-config-defaults.js.map +1 -0
  30. package/dist/src/templates/plans/default.d.ts +14 -44
  31. package/dist/src/templates/plans/default.js +10 -44
  32. package/dist/src/templates/plans/default.js.map +1 -1
  33. package/dist/src/types.d.ts +1 -15
  34. package/dist/src/workflow-guidance.config.json +366 -367
  35. package/dist/src/workflow-guidance.d.ts +1 -2
  36. package/dist/src/workflow-guidance.js +21 -166
  37. package/dist/src/workflow-guidance.js.map +1 -1
  38. package/package.json +44 -44
  39. package/dist/src/effective-config.d.ts +0 -2
  40. package/dist/src/effective-config.js +0 -10
  41. 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, withSerializedAccess, writeJsonFile } from "./io.js";
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 { getTemplateTaskDoneChoices, renderSeedTemplateText, resolveSeedPlanTemplate } from "./plan-templates.js";
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: await buildPlanWorkflowGuidance({
101
+ workflowGuidance: buildPlanWorkflowGuidance({
103
102
  taskName,
104
103
  planFile,
105
104
  plan,
106
105
  commandSource: input.parentTaskId !== undefined ? "subplan.create" : "plan.create",
107
- projectRoot: project.projectRoot,
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
- 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;
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
- if (input.patch) {
150
- applyPlanPatch(next, input.patch);
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
- 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);
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
- 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
- }
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
- 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.");
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
- 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;
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
- saveTaskMeta(task);
233
- if (previousStatus !== next.status || input.patch || changedTaskIds.length > 0 || input.appendTasks?.length) {
234
- events.unshift(buildPlanEvent("plan_changed", {
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
- previousStatus,
239
- ...(changedTaskIds.length > 0 ? { affectedPlanTaskIds: changedTaskIds } : {}),
184
+ taskId: planTask.id,
185
+ affectedPlanTaskIds: [planTask.id],
240
186
  }));
241
187
  }
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
- }));
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
- 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);
240
+ else {
241
+ delete task.meta.taskType;
274
242
  }
275
- return {
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
- 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
- });
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
- const merged = mergePlanPatchValue(target, patch);
498
- if (!isPlainPatchObject(merged)) {
499
- throw new ClawError("PROJECT_CONFIG_INVALID", "Plan patch must be a JSON object.");
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
- for (const key of Object.keys(target)) {
502
- delete target[key];
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 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";
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
- templateId: template.id,
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 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
- }));
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: effectiveTitle,
569
- templateId: template.id,
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: template.requirements ?? {
522
+ requirements: {
576
523
  summary: "",
577
524
  openQuestions: [],
578
525
  acceptanceCriteria: [],
579
526
  },
580
- tasks: compiledTasks,
581
- references: template.references ?? [],
582
- rules: template.rules ?? [],
583
- keyDecisions: template.keyDecisions ?? [],
584
- retrospective: template.retrospective ?? {
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);