@mcoda/core 0.1.37 → 0.1.40

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 (29) hide show
  1. package/dist/api/MswarmApi.d.ts +155 -0
  2. package/dist/api/MswarmApi.d.ts.map +1 -0
  3. package/dist/api/MswarmApi.js +593 -0
  4. package/dist/api/MswarmConfigStore.d.ts +53 -0
  5. package/dist/api/MswarmConfigStore.d.ts.map +1 -0
  6. package/dist/api/MswarmConfigStore.js +111 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/services/docs/DocsService.d.ts.map +1 -1
  11. package/dist/services/docs/DocsService.js +1 -11
  12. package/dist/services/estimate/VelocityService.d.ts.map +1 -1
  13. package/dist/services/estimate/VelocityService.js +1 -2
  14. package/dist/services/execution/AddTestsService.d.ts.map +1 -1
  15. package/dist/services/execution/AddTestsService.js +2 -2
  16. package/dist/services/execution/QaTasksService.d.ts.map +1 -1
  17. package/dist/services/execution/QaTasksService.js +3 -2
  18. package/dist/services/execution/WorkOnTasksService.d.ts.map +1 -1
  19. package/dist/services/execution/WorkOnTasksService.js +2 -6
  20. package/dist/services/openapi/OpenApiService.d.ts.map +1 -1
  21. package/dist/services/openapi/OpenApiService.js +1 -11
  22. package/dist/services/planning/CreateTasksService.d.ts +9 -0
  23. package/dist/services/planning/CreateTasksService.d.ts.map +1 -1
  24. package/dist/services/planning/CreateTasksService.js +490 -209
  25. package/dist/services/review/CodeReviewService.js +2 -2
  26. package/dist/services/shared/GitBranch.d.ts +6 -0
  27. package/dist/services/shared/GitBranch.d.ts.map +1 -0
  28. package/dist/services/shared/GitBranch.js +62 -0
  29. package/package.json +6 -6
@@ -219,6 +219,18 @@ const STRICT_AGENT_SINGLE_STORY_DOC_SUMMARY_TOKEN_LIMIT = 1200;
219
219
  const STRICT_AGENT_SINGLE_STORY_BUILD_METHOD_TOKEN_LIMIT = 900;
220
220
  const STRICT_AGENT_SINGLE_TASK_DOC_SUMMARY_TOKEN_LIMIT = 1200;
221
221
  const STRICT_AGENT_SINGLE_TASK_BUILD_METHOD_TOKEN_LIMIT = 900;
222
+ const STRICT_AGENT_COMPACT_TASK_STRUCTURED_PROMPT_TOKEN_LIMIT = 1200;
223
+ const STRICT_AGENT_COMPACT_TASK_RUNTIME_PROMPT_TOKEN_LIMIT = 1000;
224
+ const STRICT_AGENT_COMPACT_TASK_MINIMAL_PROMPT_TOKEN_LIMIT = 650;
225
+ const STRICT_AGENT_COMPACT_TASK_FULL_STORY_TOKEN_LIMIT = 260;
226
+ const STRICT_AGENT_COMPACT_TASK_MINIMAL_STORY_TOKEN_LIMIT = 140;
227
+ const STRICT_AGENT_COMPACT_TASK_FULL_ACCEPTANCE_TOKEN_LIMIT = 180;
228
+ const STRICT_AGENT_COMPACT_TASK_MINIMAL_ACCEPTANCE_TOKEN_LIMIT = 90;
229
+ const STRICT_AGENT_COMPACT_TASK_FULL_DOC_TOKEN_LIMIT = 140;
230
+ const STRICT_AGENT_COMPACT_TASK_MINIMAL_DOC_TOKEN_LIMIT = 60;
231
+ const STRICT_AGENT_COMPACT_TASK_FULL_BUILD_TOKEN_LIMIT = 160;
232
+ const STRICT_AGENT_COMPACT_TASK_MINIMAL_BUILD_TOKEN_LIMIT = 70;
233
+ const STRICT_AGENT_MAX_TASKS_PER_COMPACT_REWRITE = 3;
222
234
  const META_TASK_PATTERN = /\b(plan|planning|backlog|coverage|artifact|evidence capture|document baseline|update refine log|record refinement|review inputs)\b/i;
223
235
  const compactPromptContext = (value, maxTokens, fallback = "none") => {
224
236
  const text = value?.trim();
@@ -1253,7 +1265,12 @@ const TASK_COMPACT_SCHEMA_SNIPPET = `{
1253
1265
  "files": ["relative/path/to/implementation.file"],
1254
1266
  "estimatedStoryPoints": 3,
1255
1267
  "priorityHint": 50,
1256
- "dependsOnKeys": ["t0"]
1268
+ "dependsOnKeys": ["t0"],
1269
+ "relatedDocs": ["docdex:..."],
1270
+ "unitTests": ["unit test description"],
1271
+ "componentTests": ["component test description"],
1272
+ "integrationTests": ["integration test description"],
1273
+ "apiTests": ["api test description"]
1257
1274
  }
1258
1275
  ]
1259
1276
  }`;
@@ -1855,7 +1872,34 @@ export class CreateTasksService {
1855
1872
  .join("\n"), 96))
1856
1873
  .map((value) => this.normalizeStructurePathToken(value))
1857
1874
  .filter((value) => Boolean(value));
1858
- return uniqueStrings([...explicitFiles, ...extractedFiles]).slice(0, 8);
1875
+ return this.preferSpecificTaskTargets([...explicitFiles, ...extractedFiles]).slice(0, 8);
1876
+ }
1877
+ preferSpecificTaskTargets(targets) {
1878
+ const normalized = uniqueStrings(targets
1879
+ .map((value) => this.normalizeStructurePathToken(value) ?? value.replace(/\\/g, "/").trim())
1880
+ .filter((value) => Boolean(value)));
1881
+ const sorted = normalized.sort((left, right) => {
1882
+ const leftIsFile = isStructuredFilePath(path.basename(left));
1883
+ const rightIsFile = isStructuredFilePath(path.basename(right));
1884
+ if (leftIsFile !== rightIsFile)
1885
+ return leftIsFile ? -1 : 1;
1886
+ const leftDepth = left.split("/").filter(Boolean).length;
1887
+ const rightDepth = right.split("/").filter(Boolean).length;
1888
+ if (leftDepth !== rightDepth)
1889
+ return rightDepth - leftDepth;
1890
+ if (left.length !== right.length)
1891
+ return right.length - left.length;
1892
+ return left.localeCompare(right);
1893
+ });
1894
+ const kept = [];
1895
+ for (const target of sorted) {
1896
+ const prefix = `${target.replace(/\/+$/g, "")}/`;
1897
+ if (kept.some((existing) => existing === target || existing.startsWith(prefix))) {
1898
+ continue;
1899
+ }
1900
+ kept.push(target);
1901
+ }
1902
+ return kept.sort((left, right) => left.length - right.length || left.localeCompare(right));
1859
1903
  }
1860
1904
  extractStructureTargets(docs) {
1861
1905
  const directories = new Set();
@@ -5532,6 +5576,11 @@ export class CreateTasksService {
5532
5576
  "estimatedStoryPoints",
5533
5577
  "priorityHint",
5534
5578
  "dependsOnKeys",
5579
+ "relatedDocs",
5580
+ "unitTests",
5581
+ "componentTests",
5582
+ "integrationTests",
5583
+ "apiTests",
5535
5584
  ],
5536
5585
  properties: {
5537
5586
  localId: nullableString,
@@ -5542,6 +5591,11 @@ export class CreateTasksService {
5542
5591
  estimatedStoryPoints: nullableNumber,
5543
5592
  priorityHint: nullableNumber,
5544
5593
  dependsOnKeys: stringArray,
5594
+ relatedDocs: stringArray,
5595
+ unitTests: stringArray,
5596
+ componentTests: stringArray,
5597
+ integrationTests: stringArray,
5598
+ apiTests: stringArray,
5545
5599
  },
5546
5600
  additionalProperties: false,
5547
5601
  };
@@ -5798,12 +5852,15 @@ export class CreateTasksService {
5798
5852
  shouldPreferSchemaFreeInitialCompactTasks() {
5799
5853
  return this.compactTaskSchemaStrategy === "schema_free_pref";
5800
5854
  }
5801
- async activateCompactTaskSchemaFallback(jobId) {
5855
+ async activateCompactTaskSchemaFallback(jobId, reason) {
5802
5856
  this.compactTaskSchemaStrategy = "schema_free_pref";
5803
5857
  if (this.compactTaskSchemaStrategyLogged)
5804
5858
  return;
5805
5859
  this.compactTaskSchemaStrategyLogged = true;
5806
5860
  await this.jobService.appendLog(jobId, "[create-tasks] tasks_compact structured mode is unstable in this run; preferring schema-free initial calls for remaining compact task prompts.\n");
5861
+ if (reason) {
5862
+ await this.jobService.appendLog(jobId, `[create-tasks] ${reason}\n`);
5863
+ }
5807
5864
  }
5808
5865
  buildJsonRepairPrompt(action, originalPrompt, originalOutput) {
5809
5866
  if (action === "tasks_compact") {
@@ -6603,6 +6660,12 @@ export class CreateTasksService {
6603
6660
  let output = "";
6604
6661
  let invocationMetadata;
6605
6662
  const currentTimestamp = () => new Date().toISOString();
6663
+ const promptTokens = estimateTokens(prompt);
6664
+ if (action === "tasks_compact" &&
6665
+ !this.shouldPreferSchemaFreeInitialCompactTasks() &&
6666
+ promptTokens > STRICT_AGENT_COMPACT_TASK_STRUCTURED_PROMPT_TOKEN_LIMIT) {
6667
+ await this.activateCompactTaskSchemaFallback(jobId, `tasks_compact prompt estimate ${promptTokens} exceeds structured reliability limit ${STRICT_AGENT_COMPACT_TASK_STRUCTURED_PROMPT_TOKEN_LIMIT}.`);
6668
+ }
6606
6669
  const actionOutputSchema = this.outputSchemaForAction(action);
6607
6670
  const preferSchemaFreeInitialCompactCall = action === "tasks_compact" && this.shouldPreferSchemaFreeInitialCompactTasks();
6608
6671
  const outputSchema = preferSchemaFreeInitialCompactCall ? undefined : actionOutputSchema;
@@ -7025,6 +7088,10 @@ export class CreateTasksService {
7025
7088
  await this.jobService.appendLog(jobId, `Strict story repair failed for epic "${epic.title}". Using deterministic fallback story and continuing. Reason: ${repairMessage}\n`);
7026
7089
  stories = [this.buildFallbackStoryForEpic(epic)];
7027
7090
  }
7091
+ if (stories.length === 0) {
7092
+ await this.jobService.appendLog(jobId, `Strict story repair returned no stories for epic "${epic.title}". Using deterministic fallback story and continuing.\n`);
7093
+ stories = [this.buildFallbackStoryForEpic(epic)];
7094
+ }
7028
7095
  }
7029
7096
  if (stories.length === 0) {
7030
7097
  await this.jobService.appendLog(jobId, `Story generation returned no stories for epic "${epic.title}". Retrying through strict staged recovery.\n`);
@@ -7036,6 +7103,10 @@ export class CreateTasksService {
7036
7103
  await this.jobService.appendLog(jobId, `Strict story repair failed for epic "${epic.title}" after empty output. Using deterministic fallback story and continuing. Reason: ${repairMessage}\n`);
7037
7104
  stories = [this.buildFallbackStoryForEpic(epic)];
7038
7105
  }
7106
+ if (stories.length === 0) {
7107
+ await this.jobService.appendLog(jobId, `Strict story repair returned no stories for epic "${epic.title}" after empty output. Using deterministic fallback story and continuing.\n`);
7108
+ stories = [this.buildFallbackStoryForEpic(epic)];
7109
+ }
7039
7110
  }
7040
7111
  storiesByEpic.set(epic.localId, stories);
7041
7112
  return;
@@ -7061,6 +7132,7 @@ export class CreateTasksService {
7061
7132
  if (chunk.length === 1) {
7062
7133
  const { epic, story } = chunk[0];
7063
7134
  const storyScope = this.storyScopeKey(story.epicLocalId, story.localId);
7135
+ const fallbackTasks = this.buildFallbackTasksForStory(story);
7064
7136
  let tasks;
7065
7137
  try {
7066
7138
  tasks = await this.generateTasksForStory(agent, projectKey, { key: epic.localId, title: epicTitleByLocalId.get(epic.localId) ?? epic.title }, { ...story, key: story.localId }, docSummary, projectBuildMethod, stream, jobId, commandRunId, { compactSchema: options?.compactSingleStorySchema === true });
@@ -7068,30 +7140,45 @@ export class CreateTasksService {
7068
7140
  catch (error) {
7069
7141
  const message = error.message ?? String(error);
7070
7142
  if (this.isAgentTimeoutLikeError(error)) {
7071
- await this.jobService.appendLog(jobId, `Task generation timed out for story "${story.title}" (${storyScope}). Using deterministic fallback tasks without a second repair attempt.\n`);
7072
- tasks = this.buildFallbackTasksForStory(story);
7143
+ await this.jobService.appendLog(jobId, `Task generation timed out for story "${story.title}" (${storyScope}). Retrying through strict staged recovery before deterministic fallback.\n`);
7144
+ try {
7145
+ tasks = await this.repairTasksForStory(agent, projectKey, { key: epic.localId, title: epicTitleByLocalId.get(epic.localId) ?? epic.title }, { ...story, key: story.localId }, docSummary, projectBuildMethod, message, fallbackTasks, stream, jobId, commandRunId, { compactSchema: options?.compactSingleStorySchema === true });
7146
+ }
7147
+ catch (repairError) {
7148
+ const repairMessage = repairError.message ?? String(repairError);
7149
+ await this.jobService.appendLog(jobId, `Strict task repair failed for story "${story.title}" (${storyScope}) after timeout. Using deterministic fallback tasks and continuing. Reason: ${repairMessage}\n`);
7150
+ tasks = fallbackTasks;
7151
+ }
7073
7152
  tasksByStoryScope.set(storyScope, tasks);
7074
7153
  return;
7075
7154
  }
7076
7155
  await this.jobService.appendLog(jobId, `Task generation failed for story "${story.title}" (${storyScope}). Retrying through strict staged recovery. Reason: ${message}\n`);
7077
7156
  try {
7078
- tasks = await this.repairTasksForStory(agent, projectKey, { key: epic.localId, title: epicTitleByLocalId.get(epic.localId) ?? epic.title }, { ...story, key: story.localId }, docSummary, projectBuildMethod, message, this.buildFallbackTasksForStory(story), stream, jobId, commandRunId, { compactSchema: options?.compactSingleStorySchema === true });
7157
+ tasks = await this.repairTasksForStory(agent, projectKey, { key: epic.localId, title: epicTitleByLocalId.get(epic.localId) ?? epic.title }, { ...story, key: story.localId }, docSummary, projectBuildMethod, message, fallbackTasks, stream, jobId, commandRunId, { compactSchema: options?.compactSingleStorySchema === true });
7079
7158
  }
7080
7159
  catch (repairError) {
7081
7160
  const repairMessage = repairError.message ?? String(repairError);
7082
7161
  await this.jobService.appendLog(jobId, `Strict task repair failed for story "${story.title}" (${storyScope}). Using deterministic fallback tasks and continuing. Reason: ${repairMessage}\n`);
7083
- tasks = this.buildFallbackTasksForStory(story);
7162
+ tasks = fallbackTasks;
7163
+ }
7164
+ if (tasks.length === 0) {
7165
+ await this.jobService.appendLog(jobId, `Strict task repair returned no tasks for story "${story.title}" (${storyScope}). Using deterministic fallback tasks and continuing.\n`);
7166
+ tasks = fallbackTasks;
7084
7167
  }
7085
7168
  }
7086
7169
  if (tasks.length === 0) {
7087
7170
  await this.jobService.appendLog(jobId, `Task generation returned no tasks for story "${story.title}" (${storyScope}). Retrying through strict staged recovery.\n`);
7088
7171
  try {
7089
- tasks = await this.repairTasksForStory(agent, projectKey, { key: epic.localId, title: epicTitleByLocalId.get(epic.localId) ?? epic.title }, { ...story, key: story.localId }, docSummary, projectBuildMethod, `No tasks were returned for story ${story.title}.`, this.buildFallbackTasksForStory(story), stream, jobId, commandRunId, { compactSchema: options?.compactSingleStorySchema === true });
7172
+ tasks = await this.repairTasksForStory(agent, projectKey, { key: epic.localId, title: epicTitleByLocalId.get(epic.localId) ?? epic.title }, { ...story, key: story.localId }, docSummary, projectBuildMethod, `No tasks were returned for story ${story.title}.`, fallbackTasks, stream, jobId, commandRunId, { compactSchema: options?.compactSingleStorySchema === true });
7090
7173
  }
7091
7174
  catch (repairError) {
7092
7175
  const repairMessage = repairError.message ?? String(repairError);
7093
7176
  await this.jobService.appendLog(jobId, `Strict task repair failed for story "${story.title}" (${storyScope}) after empty output. Using deterministic fallback tasks and continuing. Reason: ${repairMessage}\n`);
7094
- tasks = this.buildFallbackTasksForStory(story);
7177
+ tasks = fallbackTasks;
7178
+ }
7179
+ if (tasks.length === 0) {
7180
+ await this.jobService.appendLog(jobId, `Strict task repair returned no tasks for story "${story.title}" (${storyScope}) after empty output. Using deterministic fallback tasks and continuing.\n`);
7181
+ tasks = fallbackTasks;
7095
7182
  }
7096
7183
  }
7097
7184
  tasksByStoryScope.set(storyScope, tasks);
@@ -7157,15 +7244,11 @@ export class CreateTasksService {
7157
7244
  const seedTasks = this.buildFallbackTasksForStory(story);
7158
7245
  const compactBuildMethod = compactPromptContext(projectBuildMethod, compactSchema ? 220 : STRICT_AGENT_SINGLE_TASK_BUILD_METHOD_TOKEN_LIMIT, "none");
7159
7246
  const compactDocSummary = compactPromptContext(docSummary, compactSchema ? 180 : STRICT_AGENT_SINGLE_TASK_DOC_SUMMARY_TOKEN_LIMIT, "none");
7247
+ if (compactSchema) {
7248
+ return this.executeCompactTaskRewrite(agent, projectKey, epic, story, compactDocSummary, compactBuildMethod, seedTasks, stream, jobId, commandRunId);
7249
+ }
7160
7250
  const prompt = compactSchema
7161
- ? this.buildCompactSeededTaskPrompt({
7162
- projectKey,
7163
- epic,
7164
- story,
7165
- docSummary: compactDocSummary,
7166
- projectBuildMethod: compactBuildMethod,
7167
- seedTasks,
7168
- })
7251
+ ? ""
7169
7252
  : [
7170
7253
  this.buildCreateTasksAgentMission(projectKey),
7171
7254
  `Generate tasks for story "${story.title}" (Epic: ${epic.title}, phase 3 of 3).`,
@@ -7205,8 +7288,8 @@ export class CreateTasksService {
7205
7288
  compactBuildMethod,
7206
7289
  `Docs: ${compactDocSummary}`,
7207
7290
  ].join("\n\n");
7208
- const action = compactSchema ? "tasks_compact" : "tasks";
7209
- const taskStream = compactSchema ? false : stream;
7291
+ const action = "tasks";
7292
+ const taskStream = stream;
7210
7293
  const { output } = await this.invokeAgentWithRetry(agent, prompt, action, taskStream, jobId, commandRunId, {
7211
7294
  epicKey: epic.key,
7212
7295
  storyKey: story.key ?? story.localId,
@@ -7216,9 +7299,10 @@ export class CreateTasksService {
7216
7299
  if (!parsed || !Array.isArray(parsed.tasks) || parsed.tasks.length === 0) {
7217
7300
  throw new Error(`Agent did not return tasks for story ${story.title}`);
7218
7301
  }
7219
- return parsed.tasks
7302
+ const normalizedTasks = parsed.tasks
7220
7303
  .map((task, idx) => this.normalizeAgentTaskNode(task, idx))
7221
7304
  .filter((t) => t.title);
7305
+ return compactSchema ? this.mergeCompactTaskMetadata(normalizedTasks, seedTasks) : normalizedTasks;
7222
7306
  }
7223
7307
  async repairStoriesForEpic(agent, projectKey, epic, docSummary, projectBuildMethod, reason, seedStories, stream, jobId, commandRunId) {
7224
7308
  const compactBuildMethod = compactPromptContext(projectBuildMethod, STRICT_AGENT_SINGLE_STORY_BUILD_METHOD_TOKEN_LIMIT, "none");
@@ -7262,16 +7346,11 @@ export class CreateTasksService {
7262
7346
  const compactSchema = options?.compactSchema === true;
7263
7347
  const compactBuildMethod = compactPromptContext(projectBuildMethod, compactSchema ? 220 : STRICT_AGENT_SINGLE_TASK_BUILD_METHOD_TOKEN_LIMIT, "none");
7264
7348
  const compactDocSummary = compactPromptContext(docSummary, compactSchema ? 180 : STRICT_AGENT_SINGLE_TASK_DOC_SUMMARY_TOKEN_LIMIT, "none");
7349
+ if (compactSchema) {
7350
+ return this.executeCompactTaskRewrite(agent, projectKey, epic, story, compactDocSummary, compactBuildMethod, seedTasks, stream, jobId, commandRunId, reason);
7351
+ }
7265
7352
  const prompt = compactSchema
7266
- ? this.buildCompactSeededTaskPrompt({
7267
- projectKey,
7268
- epic,
7269
- story,
7270
- docSummary: compactDocSummary,
7271
- projectBuildMethod: compactBuildMethod,
7272
- seedTasks,
7273
- previousFailure: reason,
7274
- })
7353
+ ? ""
7275
7354
  : [
7276
7355
  this.buildCreateTasksAgentMission(projectKey),
7277
7356
  `Repair task generation for story "${story.title}" (Epic: ${epic.title}). The previous attempt failed or returned no tasks.`,
@@ -7295,8 +7374,8 @@ export class CreateTasksService {
7295
7374
  JSON.stringify({ tasks: seedTasks }, null, 2),
7296
7375
  `Docs: ${compactDocSummary}`,
7297
7376
  ].join("\n\n");
7298
- const action = compactSchema ? "tasks_compact" : "tasks";
7299
- const taskStream = compactSchema ? false : stream;
7377
+ const action = "tasks";
7378
+ const taskStream = stream;
7300
7379
  const { output } = await this.invokeAgentWithRetry(agent, prompt, action, taskStream, jobId, commandRunId, {
7301
7380
  epicKey: epic.key,
7302
7381
  storyKey: story.key ?? story.localId,
@@ -7308,9 +7387,72 @@ export class CreateTasksService {
7308
7387
  if (!parsed || !Array.isArray(parsed.tasks) || parsed.tasks.length === 0) {
7309
7388
  throw new Error(`Agent did not return repair tasks for story ${story.title}`);
7310
7389
  }
7311
- return parsed.tasks
7390
+ const normalizedTasks = parsed.tasks
7312
7391
  .map((task, idx) => this.normalizeAgentTaskNode(task, idx))
7313
7392
  .filter((task) => task.title);
7393
+ return compactSchema ? this.mergeCompactTaskMetadata(normalizedTasks, seedTasks) : normalizedTasks;
7394
+ }
7395
+ splitCompactTaskRewriteChunks(seedTasks) {
7396
+ if (seedTasks.length <= STRICT_AGENT_MAX_TASKS_PER_COMPACT_REWRITE) {
7397
+ return [seedTasks];
7398
+ }
7399
+ const chunks = [];
7400
+ for (let index = 0; index < seedTasks.length; index += STRICT_AGENT_MAX_TASKS_PER_COMPACT_REWRITE) {
7401
+ chunks.push(seedTasks.slice(index, index + STRICT_AGENT_MAX_TASKS_PER_COMPACT_REWRITE));
7402
+ }
7403
+ return chunks;
7404
+ }
7405
+ async executeCompactTaskRewrite(agent, projectKey, epic, story, docSummary, projectBuildMethod, seedTasks, stream, jobId, commandRunId, previousFailure) {
7406
+ const initialChunkCount = this.splitCompactTaskRewriteChunks(seedTasks).length;
7407
+ const taskChunks = this.planCompactTaskRewriteChunks({
7408
+ projectKey,
7409
+ epic,
7410
+ story,
7411
+ docSummary,
7412
+ projectBuildMethod,
7413
+ seedTasks,
7414
+ previousFailure,
7415
+ });
7416
+ if (taskChunks.length > initialChunkCount) {
7417
+ await this.jobService.appendLog(jobId, `[create-tasks] compact task rewrite split story "${story.title}" into ${taskChunks.length} prompt-bounded chunk(s).\n`);
7418
+ }
7419
+ if (taskChunks.some((chunk) => chunk.contextMode === "minimal")) {
7420
+ await this.jobService.appendLog(jobId, `[create-tasks] compact task rewrite is using reduced prompt context for story "${story.title}".\n`);
7421
+ }
7422
+ const rewritten = [];
7423
+ for (const [index, chunkPlan] of taskChunks.entries()) {
7424
+ const prompt = this.buildCompactSeededTaskPrompt({
7425
+ projectKey,
7426
+ epic,
7427
+ story,
7428
+ docSummary,
7429
+ projectBuildMethod,
7430
+ seedTasks: chunkPlan.seedTasks,
7431
+ previousFailure,
7432
+ chunkIndex: index,
7433
+ chunkCount: taskChunks.length,
7434
+ totalSeedTaskCount: seedTasks.length,
7435
+ contextMode: chunkPlan.contextMode,
7436
+ });
7437
+ const { output } = await this.invokeAgentWithRetry(agent, prompt, "tasks_compact", false, jobId, commandRunId, {
7438
+ epicKey: epic.key,
7439
+ storyKey: story.key ?? story.localId,
7440
+ strictAgentMode: true,
7441
+ repairStage: previousFailure ? "tasks" : undefined,
7442
+ taskChunkIndex: index + 1,
7443
+ taskChunkCount: taskChunks.length,
7444
+ timeoutMs: this.resolveStrictBatchTimeoutMs("tasks_batch", 1),
7445
+ });
7446
+ const parsed = extractJson(output);
7447
+ if (!parsed || !Array.isArray(parsed.tasks) || parsed.tasks.length === 0) {
7448
+ throw new Error(`Agent did not return compact tasks for story ${story.title} chunk ${index + 1}`);
7449
+ }
7450
+ const normalizedTasks = parsed.tasks
7451
+ .map((task, taskIndex) => this.normalizeAgentTaskNode(task, taskIndex))
7452
+ .filter((task) => task.title);
7453
+ rewritten.push(...this.mergeCompactTaskMetadata(normalizedTasks, chunkPlan.seedTasks));
7454
+ }
7455
+ return rewritten;
7314
7456
  }
7315
7457
  buildFallbackStoryForEpic(epic) {
7316
7458
  const derived = this.buildDerivedStoryForEpic(epic);
@@ -7525,7 +7667,7 @@ export class CreateTasksService {
7525
7667
  .filter(Boolean)).slice(0, 6);
7526
7668
  }
7527
7669
  normalizeStoryHintFiles(values) {
7528
- return uniqueStrings(values
7670
+ return this.preferSpecificTaskTargets(values
7529
7671
  .map((value) => this.normalizeStructurePathToken(value))
7530
7672
  .filter((value) => Boolean(value))).slice(0, 6);
7531
7673
  }
@@ -7541,9 +7683,24 @@ export class CreateTasksService {
7541
7683
  return "generic";
7542
7684
  }
7543
7685
  buildCompactSeededTaskPrompt(params) {
7544
- const compactSeedTasks = this.buildCompactTaskSeeds(params.seedTasks);
7686
+ const contextMode = params.contextMode ?? "full";
7687
+ const minimalContext = contextMode === "minimal";
7688
+ const compactSeedTasks = this.buildCompactTaskSeeds(params.seedTasks, { minimalContext });
7689
+ const chunkCount = Math.max(1, params.chunkCount ?? 1);
7690
+ const chunkIndex = Math.max(0, params.chunkIndex ?? 0);
7691
+ const totalSeedTaskCount = Math.max(compactSeedTasks.length, params.totalSeedTaskCount ?? compactSeedTasks.length);
7692
+ const chunkLocalIds = compactSeedTasks.map((task) => `${task.localId ?? ""}`.trim()).filter(Boolean);
7693
+ const storyContext = compactPromptContext(params.story.description ?? params.story.userStory ?? "", minimalContext ? STRICT_AGENT_COMPACT_TASK_MINIMAL_STORY_TOKEN_LIMIT : STRICT_AGENT_COMPACT_TASK_FULL_STORY_TOKEN_LIMIT, "none");
7694
+ const acceptanceContext = compactPromptContext((params.story.acceptanceCriteria ?? []).slice(0, minimalContext ? 3 : 5).join("; "), minimalContext
7695
+ ? STRICT_AGENT_COMPACT_TASK_MINIMAL_ACCEPTANCE_TOKEN_LIMIT
7696
+ : STRICT_AGENT_COMPACT_TASK_FULL_ACCEPTANCE_TOKEN_LIMIT, "none");
7697
+ const compactBuildMethod = compactPromptContext(params.projectBuildMethod, minimalContext ? STRICT_AGENT_COMPACT_TASK_MINIMAL_BUILD_TOKEN_LIMIT : STRICT_AGENT_COMPACT_TASK_FULL_BUILD_TOKEN_LIMIT, "none");
7698
+ const compactDocSummary = compactPromptContext(params.docSummary, minimalContext ? STRICT_AGENT_COMPACT_TASK_MINIMAL_DOC_TOKEN_LIMIT : STRICT_AGENT_COMPACT_TASK_FULL_DOC_TOKEN_LIMIT, "none");
7545
7699
  return [
7546
7700
  `Project ${params.projectKey}. Phase 3 compact task synthesis for story "${params.story.title}" in epic "${params.epic.title}".`,
7701
+ chunkCount > 1
7702
+ ? `This prompt covers compact task chunk ${chunkIndex + 1}/${chunkCount} for ${totalSeedTaskCount} total story tasks. Rewrite only the localIds in this chunk: ${chunkLocalIds.join(", ")}.`
7703
+ : null,
7547
7704
  params.previousFailure
7548
7705
  ? `The previous attempt failed: ${params.previousFailure}`
7549
7706
  : "Rewrite the provided seed tasks into the final story task list.",
@@ -7553,36 +7710,201 @@ export class CreateTasksService {
7553
7710
  `- Return exactly ${compactSeedTasks.length} tasks.`,
7554
7711
  "- Preserve the seed task localIds and dependsOnKeys unless a direct consistency fix is required.",
7555
7712
  "- Keep each returned task aligned to the corresponding seed task role and execution order.",
7713
+ chunkCount > 1
7714
+ ? "- Return tasks only for the chunk localIds listed in this prompt. Preserve dependsOnKeys even when they reference earlier-chunk localIds not rewritten here."
7715
+ : null,
7556
7716
  "- Keep the task list scoped to this story only; do not introduce cross-story dependencies.",
7557
- "- Improve titles, descriptions, file targets, and story points using the story context and seed targets below.",
7717
+ "- Improve titles, descriptions, file targets, related docs, test arrays, and story points using the story context and seed targets below.",
7558
7718
  "- Prefer exact repo-relative files from the seed tasks and story hints; only broaden to directories when no deeper target is known.",
7559
7719
  "- You do not have tool access in this subtask. Do not say you will inspect Docdex, repo files, profile memory, or any other context.",
7560
7720
  "- Do not narrate your work, explain your reasoning, or emit any prose outside the JSON object.",
7561
7721
  '- Emit the final JSON object immediately. The first character must be "{" and the last must be "}".',
7562
7722
  `Story context (key=${params.story.key ?? params.story.localId ?? "TBD"}):`,
7563
- params.story.description ?? params.story.userStory ?? "",
7564
- `Acceptance criteria: ${(params.story.acceptanceCriteria ?? []).join("; ")}`,
7723
+ storyContext,
7724
+ `Acceptance criteria: ${acceptanceContext}`,
7565
7725
  "Project construction method:",
7566
- params.projectBuildMethod,
7726
+ compactBuildMethod,
7567
7727
  "Seed tasks to rewrite exactly:",
7568
7728
  JSON.stringify({ tasks: compactSeedTasks }, null, 2),
7569
- `Docs: ${params.docSummary}`,
7729
+ `Docs: ${compactDocSummary}`,
7570
7730
  ]
7571
7731
  .filter((line) => Boolean(line))
7572
7732
  .join("\n\n");
7573
7733
  }
7574
- buildCompactTaskSeeds(tasks) {
7734
+ buildCompactTaskSeeds(tasks, options) {
7735
+ const minimalContext = options?.minimalContext === true;
7736
+ const compactDescription = (value) => {
7737
+ const collapsed = `${value ?? ""}`.replace(/\s+/g, " ").trim();
7738
+ const maxLength = minimalContext ? 160 : 280;
7739
+ if (collapsed.length <= maxLength)
7740
+ return collapsed;
7741
+ return `${collapsed.slice(0, maxLength - 3).replace(/[ ,;:.-]+$/g, "")}...`;
7742
+ };
7575
7743
  return tasks.map((task) => ({
7576
7744
  localId: task.localId,
7577
7745
  title: task.title,
7578
7746
  type: task.type,
7579
- description: task.description,
7580
- files: normalizeStringArray(task.files),
7747
+ description: compactDescription(task.description),
7748
+ files: this.preferSpecificTaskTargets(normalizeStringArray(task.files)).slice(0, minimalContext ? 4 : 8),
7581
7749
  estimatedStoryPoints: task.estimatedStoryPoints ?? null,
7582
7750
  priorityHint: task.priorityHint ?? null,
7583
7751
  dependsOnKeys: normalizeStringArray(task.dependsOnKeys),
7752
+ relatedDocs: normalizeRelatedDocs(task.relatedDocs).slice(0, minimalContext ? 2 : 4),
7753
+ unitTests: normalizeStringArray(task.unitTests).slice(0, minimalContext ? 1 : 2),
7754
+ componentTests: normalizeStringArray(task.componentTests).slice(0, minimalContext ? 1 : 2),
7755
+ integrationTests: normalizeStringArray(task.integrationTests).slice(0, minimalContext ? 1 : 2),
7756
+ apiTests: normalizeStringArray(task.apiTests).slice(0, minimalContext ? 1 : 2),
7584
7757
  }));
7585
7758
  }
7759
+ planCompactTaskRewriteChunks(params) {
7760
+ const initialContextMode = params.previousFailure ? "minimal" : "full";
7761
+ const queue = this.splitCompactTaskRewriteChunks(params.seedTasks).map((seedChunk) => ({
7762
+ seedTasks: seedChunk,
7763
+ contextMode: initialContextMode,
7764
+ }));
7765
+ const planned = [];
7766
+ while (queue.length > 0) {
7767
+ const current = queue.shift();
7768
+ const prompt = this.buildCompactSeededTaskPrompt({
7769
+ ...params,
7770
+ seedTasks: current.seedTasks,
7771
+ chunkIndex: 0,
7772
+ chunkCount: 1,
7773
+ totalSeedTaskCount: params.seedTasks.length,
7774
+ contextMode: current.contextMode,
7775
+ });
7776
+ const promptTokens = estimateTokens(prompt);
7777
+ const promptLimit = current.contextMode === "minimal"
7778
+ ? STRICT_AGENT_COMPACT_TASK_MINIMAL_PROMPT_TOKEN_LIMIT
7779
+ : STRICT_AGENT_COMPACT_TASK_RUNTIME_PROMPT_TOKEN_LIMIT;
7780
+ if (promptTokens > promptLimit && current.contextMode !== "minimal") {
7781
+ queue.unshift({ seedTasks: current.seedTasks, contextMode: "minimal" });
7782
+ continue;
7783
+ }
7784
+ if (promptTokens > promptLimit && current.seedTasks.length > 1) {
7785
+ const [left, right] = this.splitChunkInHalf(current.seedTasks);
7786
+ if (right.length > 0) {
7787
+ queue.unshift({ seedTasks: right, contextMode: "minimal" }, { seedTasks: left, contextMode: "minimal" });
7788
+ continue;
7789
+ }
7790
+ }
7791
+ planned.push(current);
7792
+ }
7793
+ return planned;
7794
+ }
7795
+ groupFallbackTaskTargets(targets, maxGroups) {
7796
+ const cleaned = this.preferSpecificTaskTargets(targets).filter((value) => Boolean(value));
7797
+ if (cleaned.length === 0 || maxGroups <= 0)
7798
+ return [];
7799
+ const groups = new Map();
7800
+ for (const target of cleaned) {
7801
+ const normalized = target.replace(/\\/g, "/");
7802
+ const parts = normalized.split("/").filter(Boolean);
7803
+ const keyParts = isStructuredFilePath(path.basename(normalized)) ? parts.slice(0, -1) : parts;
7804
+ let key = target;
7805
+ if (keyParts.length >= 4) {
7806
+ key = keyParts.slice(0, 4).join("/");
7807
+ }
7808
+ else if (keyParts.length >= 3) {
7809
+ key = keyParts.slice(0, 3).join("/");
7810
+ }
7811
+ else if (keyParts.length >= 2) {
7812
+ key = keyParts.slice(0, 2).join("/");
7813
+ }
7814
+ const existing = groups.get(key) ?? [];
7815
+ existing.push(target);
7816
+ groups.set(key, existing);
7817
+ }
7818
+ const ordered = [...groups.values()]
7819
+ .map((group) => uniqueStrings(group))
7820
+ .sort((left, right) => right.length - left.length || left[0].localeCompare(right[0]));
7821
+ if (ordered.length <= maxGroups)
7822
+ return ordered;
7823
+ const head = ordered.slice(0, Math.max(1, maxGroups - 1));
7824
+ const tail = uniqueStrings(ordered.slice(Math.max(1, maxGroups - 1)).flat());
7825
+ return [...head, tail];
7826
+ }
7827
+ summarizeFallbackTargetGroup(targets) {
7828
+ if (targets.length === 0)
7829
+ return "Target Slice";
7830
+ const titleize = (value) => value
7831
+ .split(/[\s._/-]+/)
7832
+ .filter(Boolean)
7833
+ .map((token) => token[0].toUpperCase() + token.slice(1))
7834
+ .join(" ");
7835
+ const fileNames = uniqueStrings(targets
7836
+ .map((target) => path.basename(target))
7837
+ .filter((value) => isStructuredFilePath(value))
7838
+ .map((value) => value.replace(/\.[^.]+$/, "")));
7839
+ if (fileNames.length === 1)
7840
+ return titleize(fileNames[0]);
7841
+ if (fileNames.length >= 2)
7842
+ return `${titleize(fileNames[0])} and ${titleize(fileNames[1])}`;
7843
+ const firstTarget = targets[0];
7844
+ let root = path.dirname(firstTarget);
7845
+ try {
7846
+ root = this.extractArchitectureRoot(firstTarget) ?? root;
7847
+ }
7848
+ catch {
7849
+ root = path.dirname(firstTarget);
7850
+ }
7851
+ const label = root.split("/").filter(Boolean).slice(-2).join(" ");
7852
+ return titleize(label || firstTarget);
7853
+ }
7854
+ buildFallbackTestMetadata(storyTitle, targets) {
7855
+ const unitTests = [];
7856
+ const componentTests = [];
7857
+ const integrationTests = [];
7858
+ const apiTests = [];
7859
+ for (const target of uniqueStrings(targets).slice(0, 4)) {
7860
+ const lower = target.toLowerCase();
7861
+ const statement = `Exercise ${target} for ${storyTitle}.`;
7862
+ if (/\b(api|rpc|gateway|provider|endpoint)\b/.test(lower)) {
7863
+ apiTests.push(statement);
7864
+ }
7865
+ else if (/\b(component|screen|page|view|ui)\b/.test(lower)) {
7866
+ componentTests.push(statement);
7867
+ }
7868
+ else if (/\b(test|spec|scenario|e2e|integration|workflow|script|runbook|deploy)\b/.test(lower)) {
7869
+ integrationTests.push(statement);
7870
+ }
7871
+ else {
7872
+ unitTests.push(statement);
7873
+ }
7874
+ }
7875
+ if (unitTests.length === 0 && componentTests.length === 0 && integrationTests.length === 0 && apiTests.length === 0) {
7876
+ integrationTests.push(`Execute focused readiness coverage for ${storyTitle}.`);
7877
+ }
7878
+ return { unitTests, componentTests, integrationTests, apiTests };
7879
+ }
7880
+ mergeCompactTaskMetadata(tasks, seedTasks) {
7881
+ if (tasks.length === 0 || seedTasks.length === 0)
7882
+ return tasks;
7883
+ const seedByLocalId = new Map(seedTasks.map((task) => [task.localId, task]));
7884
+ return tasks.map((task, index) => {
7885
+ const seed = (task.localId ? seedByLocalId.get(task.localId) : undefined) ?? seedTasks[index];
7886
+ if (!seed)
7887
+ return task;
7888
+ const mergedFiles = this.preferSpecificTaskTargets([
7889
+ ...normalizeStringArray(task.files),
7890
+ ...normalizeStringArray(seed.files),
7891
+ ]).slice(0, 8);
7892
+ const taskRelatedDocs = normalizeRelatedDocs(task.relatedDocs);
7893
+ const taskUnitTests = normalizeStringArray(task.unitTests);
7894
+ const taskComponentTests = normalizeStringArray(task.componentTests);
7895
+ const taskIntegrationTests = normalizeStringArray(task.integrationTests);
7896
+ const taskApiTests = normalizeStringArray(task.apiTests);
7897
+ return {
7898
+ ...task,
7899
+ files: mergedFiles.length > 0 ? mergedFiles : normalizeStringArray(seed.files),
7900
+ relatedDocs: taskRelatedDocs.length > 0 ? taskRelatedDocs : normalizeRelatedDocs(seed.relatedDocs),
7901
+ unitTests: taskUnitTests.length > 0 ? taskUnitTests : normalizeStringArray(seed.unitTests),
7902
+ componentTests: taskComponentTests.length > 0 ? taskComponentTests : normalizeStringArray(seed.componentTests),
7903
+ integrationTests: taskIntegrationTests.length > 0 ? taskIntegrationTests : normalizeStringArray(seed.integrationTests),
7904
+ apiTests: taskApiTests.length > 0 ? taskApiTests : normalizeStringArray(seed.apiTests),
7905
+ };
7906
+ });
7907
+ }
7586
7908
  buildFallbackTasksForStory(story) {
7587
7909
  const mode = this.classifyDerivedStoryMode(story);
7588
7910
  const primaryTargets = this.extractStoryHintList(story.description, "Primary implementation targets");
@@ -7628,197 +7950,146 @@ export class CreateTasksService {
7628
7950
  : defaultSupportingFiles.length > 0
7629
7951
  ? defaultSupportingFiles
7630
7952
  : fallbackFiles;
7953
+ const taskGroups = [];
7954
+ const pushGroups = (kind, groups) => {
7955
+ for (const files of groups) {
7956
+ if (files.length > 0)
7957
+ taskGroups.push({ kind, files });
7958
+ }
7959
+ };
7960
+ const verificationSeedFiles = defaultVerificationFiles.length > 0 ? defaultVerificationFiles : uniqueStrings([...defaultSupportingFiles, ...defaultCoreFiles]);
7631
7961
  if (mode === "verification") {
7632
- return [
7633
- {
7634
- localId: "t-fallback-1",
7635
- title: `Implement ${story.title} verification surfaces`,
7636
- type: "feature",
7637
- description: [
7638
- `Implement the concrete verification and readiness surfaces for story "${story.title}".`,
7639
- `Primary objective: ${objectiveLine}`,
7640
- verificationLine,
7641
- "Add or update the harness, assertions, fixtures, or operational hooks that make the story verifiable.",
7642
- criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.",
7643
- ].join("\n"),
7644
- files: defaultVerificationFiles,
7645
- estimatedStoryPoints: 3,
7646
- priorityHint: 1,
7647
- dependsOnKeys: [],
7648
- relatedDocs: story.relatedDocs ?? [],
7649
- unitTests: [],
7650
- componentTests: [],
7651
- integrationTests: [],
7652
- apiTests: [],
7653
- },
7654
- {
7655
- localId: "t-fallback-2",
7656
- title: `Exercise ${story.title} regression paths`,
7657
- type: "feature",
7658
- description: [
7659
- `Exercise the runtime, dependency, and failure paths covered by "${story.title}" after the verification surfaces exist.`,
7660
- supportingLine,
7661
- verificationLine,
7662
- "Ensure the verification path covers the documented completion signals and dependency order.",
7663
- ].join("\n"),
7664
- files: uniqueStrings([...defaultVerificationFiles, ...defaultSupportingFiles]).slice(0, 6),
7665
- estimatedStoryPoints: 2,
7666
- priorityHint: 2,
7667
- dependsOnKeys: ["t-fallback-1"],
7668
- relatedDocs: story.relatedDocs ?? [],
7669
- unitTests: [],
7670
- componentTests: [],
7671
- integrationTests: [],
7672
- apiTests: [],
7673
- },
7674
- {
7675
- localId: "t-fallback-3",
7676
- title: `Finalize ${story.title} readiness evidence`,
7677
- type: "chore",
7678
- description: [
7679
- `Finalize readiness evidence for "${story.title}" once the verification path is executable.`,
7680
- verificationLine,
7681
- "Capture the focused regression signals, residual risks, and release-readiness artifacts required by the story.",
7682
- ].join("\n"),
7683
- files: defaultVerificationFiles,
7684
- estimatedStoryPoints: 1,
7685
- priorityHint: 3,
7686
- dependsOnKeys: ["t-fallback-2"],
7687
- relatedDocs: story.relatedDocs ?? [],
7688
- unitTests: [],
7689
- componentTests: [],
7690
- integrationTests: [],
7691
- apiTests: [],
7692
- },
7693
- ];
7962
+ pushGroups("primary", this.groupFallbackTaskTargets(verificationSeedFiles, 2));
7963
+ pushGroups("supporting", this.groupFallbackTaskTargets(defaultSupportingFiles, 1));
7694
7964
  }
7695
- if (mode === "integration") {
7696
- return [
7697
- {
7698
- localId: "t-fallback-1",
7699
- title: `Implement ${story.title} dependency wiring`,
7965
+ else if (mode === "integration") {
7966
+ pushGroups("primary", this.groupFallbackTaskTargets(defaultSupportingFiles, 2));
7967
+ pushGroups("supporting", this.groupFallbackTaskTargets(uniqueStrings([...defaultCoreFiles, ...defaultSupportingFiles]), 1));
7968
+ }
7969
+ else {
7970
+ pushGroups("primary", this.groupFallbackTaskTargets(defaultCoreFiles, 2));
7971
+ pushGroups("supporting", this.groupFallbackTaskTargets(defaultSupportingFiles, 2));
7972
+ }
7973
+ pushGroups("verification", this.groupFallbackTaskTargets(verificationSeedFiles, mode === "verification" ? 1 : 2));
7974
+ if (taskGroups.length === 0) {
7975
+ taskGroups.push({ kind: "primary", files: fallbackFiles.slice(0, 3) });
7976
+ taskGroups.push({ kind: "verification", files: fallbackFiles.slice(0, 3) });
7977
+ }
7978
+ const dedupedGroups = [];
7979
+ const seenGroupKeys = new Set();
7980
+ for (const group of taskGroups) {
7981
+ const key = `${group.kind}:${group.files.join("|")}`;
7982
+ if (seenGroupKeys.has(key))
7983
+ continue;
7984
+ seenGroupKeys.add(key);
7985
+ dedupedGroups.push(group);
7986
+ }
7987
+ const boundedGroups = this.boundFallbackTaskGroups(dedupedGroups, mode);
7988
+ return boundedGroups.map((group, index) => {
7989
+ const label = this.summarizeFallbackTargetGroup(group.files);
7990
+ const dependsOnKeys = index > 0 ? [`t-fallback-${index}`] : [];
7991
+ const acceptanceBlock = index === 0 && criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.";
7992
+ if (group.kind === "primary") {
7993
+ return {
7994
+ localId: `t-fallback-${index + 1}`,
7995
+ title: mode === "verification"
7996
+ ? `Build ${label} verification surfaces for ${story.title}`
7997
+ : `Implement ${label} for ${story.title}`,
7700
7998
  type: "feature",
7701
7999
  description: [
7702
- `Implement the dependency and integration surfaces for story "${story.title}".`,
8000
+ `Implement the core product behavior for story "${story.title}".`,
7703
8001
  `Primary objective: ${objectiveLine}`,
7704
- supportingLine,
7705
- "Wire the documented upstream/downstream modules and runtime contracts in the required order.",
7706
- criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.",
8002
+ `Focused targets: ${group.files.join(", ")}.`,
8003
+ group.kind === "primary" && mode === "integration"
8004
+ ? supportingLine
8005
+ : primaryLine,
8006
+ "Create or update the concrete modules and baseline execution path for this target group before downstream wiring.",
8007
+ acceptanceBlock,
7707
8008
  ].join("\n"),
7708
- files: defaultSupportingFiles,
7709
- estimatedStoryPoints: 3,
7710
- priorityHint: 1,
7711
- dependsOnKeys: [],
8009
+ files: group.files,
8010
+ estimatedStoryPoints: group.files.length > 2 ? 5 : 3,
8011
+ priorityHint: Math.max(1, 100 - index * 10),
8012
+ dependsOnKeys,
7712
8013
  relatedDocs: story.relatedDocs ?? [],
7713
8014
  unitTests: [],
7714
8015
  componentTests: [],
7715
8016
  integrationTests: [],
7716
8017
  apiTests: [],
7717
- },
7718
- {
7719
- localId: "t-fallback-2",
7720
- title: `Align ${story.title} runtime contracts`,
8018
+ };
8019
+ }
8020
+ if (group.kind === "supporting") {
8021
+ return {
8022
+ localId: `t-fallback-${index + 1}`,
8023
+ title: `Wire ${label} into ${story.title}`,
7721
8024
  type: "feature",
7722
8025
  description: [
7723
- `Align the data contracts, orchestration flow, and runtime boundaries for "${story.title}" after dependency wiring lands.`,
7724
- primaryLine,
8026
+ `Integrate the supporting runtime and dependency surfaces for "${story.title}" after the prerequisite target groups are in place.`,
8027
+ `Focused targets: ${group.files.join(", ")}.`,
7725
8028
  supportingLine,
7726
- "Update compatibility seams so the integrated path is stable and dependency rationale is explicit.",
7727
- ].join("\n"),
7728
- files: uniqueStrings([...defaultSupportingFiles, ...defaultCoreFiles]).slice(0, 6),
7729
- estimatedStoryPoints: 2,
7730
- priorityHint: 2,
7731
- dependsOnKeys: ["t-fallback-1"],
7732
- relatedDocs: story.relatedDocs ?? [],
7733
- unitTests: [],
7734
- componentTests: [],
7735
- integrationTests: [],
7736
- apiTests: [],
7737
- },
7738
- {
7739
- localId: "t-fallback-3",
7740
- title: `Verify ${story.title} integrated behavior`,
7741
- type: "chore",
7742
- description: [
7743
- `Verify the integrated behavior and readiness surface for "${story.title}" after dependency alignment completes.`,
7744
- verificationLine,
7745
- "Add or update the focused validation path that proves the integration behaves in documented order.",
8029
+ "Align internal/external interfaces, dependency order, and runtime contracts across this target group.",
7746
8030
  ].join("\n"),
7747
- files: defaultVerificationFiles.length > 0
7748
- ? defaultVerificationFiles
7749
- : uniqueStrings([...defaultSupportingFiles, ...defaultCoreFiles]).slice(0, 6),
7750
- estimatedStoryPoints: 2,
7751
- priorityHint: 3,
7752
- dependsOnKeys: ["t-fallback-2"],
8031
+ files: group.files,
8032
+ estimatedStoryPoints: group.files.length > 2 ? 3 : 2,
8033
+ priorityHint: Math.max(1, 90 - index * 10),
8034
+ dependsOnKeys,
7753
8035
  relatedDocs: story.relatedDocs ?? [],
7754
8036
  unitTests: [],
7755
8037
  componentTests: [],
7756
8038
  integrationTests: [],
7757
8039
  apiTests: [],
7758
- },
7759
- ];
7760
- }
7761
- return [
7762
- {
7763
- localId: "t-fallback-1",
7764
- title: mode === "core" ? `Implement ${story.title} target modules` : `Implement core scope for ${story.title}`,
7765
- type: "feature",
7766
- description: [
7767
- `Implement the core product behavior for story "${story.title}".`,
7768
- `Primary objective: ${objectiveLine}`,
7769
- primaryLine,
7770
- "Create or update concrete modules/files and wire baseline runtime paths first.",
7771
- criteriaLines ? `Acceptance criteria to satisfy:\n${criteriaLines}` : "Acceptance criteria: use story definition.",
7772
- ].join("\n"),
7773
- files: defaultCoreFiles,
7774
- estimatedStoryPoints: 3,
7775
- priorityHint: 1,
7776
- dependsOnKeys: [],
7777
- relatedDocs: story.relatedDocs ?? [],
7778
- unitTests: [],
7779
- componentTests: [],
7780
- integrationTests: [],
7781
- apiTests: [],
7782
- },
7783
- {
7784
- localId: "t-fallback-2",
7785
- title: mode === "core" ? `Wire ${story.title} supporting dependencies` : `Integrate dependencies for ${story.title}`,
7786
- type: "feature",
7787
- description: [
7788
- `Integrate dependent interfaces and runtime dependencies for "${story.title}" after core scope implementation.`,
7789
- supportingLine,
7790
- "Align internal/external interfaces, data shapes, and dependency wiring with the documented context.",
7791
- ].join("\n"),
7792
- files: defaultSupportingFiles,
7793
- estimatedStoryPoints: 3,
7794
- priorityHint: 2,
7795
- dependsOnKeys: ["t-fallback-1"],
7796
- relatedDocs: story.relatedDocs ?? [],
7797
- unitTests: [],
7798
- componentTests: [],
7799
- integrationTests: [],
7800
- apiTests: [],
7801
- },
7802
- {
7803
- localId: "t-fallback-3",
7804
- title: `Validate ${story.title} regressions and readiness`,
8040
+ };
8041
+ }
8042
+ const testMetadata = this.buildFallbackTestMetadata(story.title, group.files);
8043
+ return {
8044
+ localId: `t-fallback-${index + 1}`,
8045
+ title: `Validate ${label} for ${story.title}`,
7805
8046
  type: "chore",
7806
8047
  description: [
7807
- `Validate "${story.title}" end-to-end with focused regression coverage and readiness evidence.`,
8048
+ `Validate the completed story slice for "${story.title}" with focused regression coverage and readiness evidence.`,
8049
+ `Focused verification targets: ${group.files.join(", ")}.`,
7808
8050
  verificationLine,
7809
- "Add/update targeted tests and verification scripts tied to implemented behavior.",
8051
+ "Add or update the targeted verification path that proves this slice behaves correctly after implementation and wiring land.",
7810
8052
  ].join("\n"),
7811
- files: defaultVerificationFiles,
8053
+ files: group.files,
7812
8054
  estimatedStoryPoints: 2,
7813
- priorityHint: 3,
7814
- dependsOnKeys: ["t-fallback-2"],
8055
+ priorityHint: Math.max(1, 80 - index * 10),
8056
+ dependsOnKeys,
7815
8057
  relatedDocs: story.relatedDocs ?? [],
7816
- unitTests: [],
7817
- componentTests: [],
7818
- integrationTests: [],
7819
- apiTests: [],
7820
- },
7821
- ];
8058
+ unitTests: testMetadata.unitTests,
8059
+ componentTests: testMetadata.componentTests,
8060
+ integrationTests: testMetadata.integrationTests,
8061
+ apiTests: testMetadata.apiTests,
8062
+ };
8063
+ });
8064
+ }
8065
+ boundFallbackTaskGroups(groups, mode) {
8066
+ const budgets = mode === "verification"
8067
+ ? { primary: 1, supporting: 1, verification: 1 }
8068
+ : { primary: 2, supporting: 1, verification: 1 };
8069
+ const mergedByKind = new Map();
8070
+ const passThroughCounts = new Map();
8071
+ const bounded = [];
8072
+ for (const group of groups) {
8073
+ const budget = budgets[group.kind];
8074
+ const passthroughBudget = Math.max(0, budget - 1);
8075
+ const currentCount = passThroughCounts.get(group.kind) ?? 0;
8076
+ if (currentCount < passthroughBudget) {
8077
+ bounded.push(group);
8078
+ passThroughCounts.set(group.kind, currentCount + 1);
8079
+ continue;
8080
+ }
8081
+ const existing = mergedByKind.get(group.kind) ?? [];
8082
+ mergedByKind.set(group.kind, [...existing, ...group.files]);
8083
+ }
8084
+ for (const [kind, files] of mergedByKind.entries()) {
8085
+ if (files.length === 0)
8086
+ continue;
8087
+ bounded.push({
8088
+ kind,
8089
+ files: this.preferSpecificTaskTargets(files).slice(0, 6),
8090
+ });
8091
+ }
8092
+ return bounded;
7822
8093
  }
7823
8094
  async generatePlanFromAgent(projectKey, epics, agent, docSummary, options) {
7824
8095
  const planEpics = epics.map((epic, idx) => ({
@@ -7920,8 +8191,13 @@ export class CreateTasksService {
7920
8191
  limitedStories = [this.buildFallbackStoryForEpic(epic)];
7921
8192
  }
7922
8193
  else {
8194
+ const fallbackStories = [this.buildFallbackStoryForEpic(epic)];
7923
8195
  await this.jobService.appendLog(options.jobId, `Story generation returned no stories for epic "${epic.title}". Retrying through strict staged recovery.\n`);
7924
- limitedStories = (await this.repairStoriesForEpic(agent, projectKey, { ...epic }, docSummary, options.projectBuildMethod, `No stories were returned for epic ${epic.title}.`, [this.buildFallbackStoryForEpic(epic)], options.agentStream, options.jobId, options.commandRunId)).slice(0, options.maxStoriesPerEpic ?? Number.MAX_SAFE_INTEGER);
8196
+ limitedStories = (await this.repairStoriesForEpic(agent, projectKey, { ...epic }, docSummary, options.projectBuildMethod, `No stories were returned for epic ${epic.title}.`, fallbackStories, options.agentStream, options.jobId, options.commandRunId)).slice(0, options.maxStoriesPerEpic ?? Number.MAX_SAFE_INTEGER);
8197
+ if (limitedStories.length === 0) {
8198
+ await this.jobService.appendLog(options.jobId, `Strict story repair returned no stories for epic "${epic.title}" after empty output. Using deterministic fallback story.\n`);
8199
+ limitedStories = fallbackStories.slice(0, options.maxStoriesPerEpic ?? Number.MAX_SAFE_INTEGER);
8200
+ }
7925
8201
  }
7926
8202
  }
7927
8203
  limitedStories.forEach((story, idx) => {
@@ -7971,11 +8247,16 @@ export class CreateTasksService {
7971
8247
  limitedTasks = this.buildFallbackTasksForStory(story).slice(0, options.maxTasksPerStory ?? Number.MAX_SAFE_INTEGER);
7972
8248
  }
7973
8249
  else {
8250
+ const fallbackTasks = this.buildFallbackTasksForStory(story);
7974
8251
  await this.jobService.appendLog(options.jobId, `Task generation returned no tasks for story "${story.title}" (${storyScope}). Retrying through strict staged recovery.\n`);
7975
8252
  limitedTasks = (await this.repairTasksForStory(agent, projectKey, {
7976
8253
  key: story.epicLocalId,
7977
8254
  title: epicTitleByLocalId.get(story.epicLocalId) ?? story.title,
7978
- }, story, docSummary, options.projectBuildMethod, `No tasks were returned for story ${story.title}.`, this.buildFallbackTasksForStory(story), options.agentStream, options.jobId, options.commandRunId)).slice(0, options.maxTasksPerStory ?? Number.MAX_SAFE_INTEGER);
8255
+ }, story, docSummary, options.projectBuildMethod, `No tasks were returned for story ${story.title}.`, fallbackTasks, options.agentStream, options.jobId, options.commandRunId)).slice(0, options.maxTasksPerStory ?? Number.MAX_SAFE_INTEGER);
8256
+ if (limitedTasks.length === 0) {
8257
+ await this.jobService.appendLog(options.jobId, `Strict task repair returned no tasks for story "${story.title}" (${storyScope}) after empty output. Using deterministic fallback tasks.\n`);
8258
+ limitedTasks = fallbackTasks.slice(0, options.maxTasksPerStory ?? Number.MAX_SAFE_INTEGER);
8259
+ }
7979
8260
  }
7980
8261
  }
7981
8262
  limitedTasks.forEach((task, idx) => {