takomi 2.1.15 → 2.1.16

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.
@@ -404,6 +404,39 @@ async function findExistingTaskFile(paths: ReturnType<typeof getSessionPaths>, t
404
404
  return undefined;
405
405
  }
406
406
 
407
+ function markdownPreservationScore(content: string): number {
408
+ const trimmed = content.trim();
409
+ if (!trimmed) return 0;
410
+ const lines = trimmed.split(/\r?\n/);
411
+ const headingCount = lines.filter((line) => /^#{1,6}\s+/.test(line)).length;
412
+ const checklistCount = lines.filter((line) => /^\s*- \[[ xX]\]/.test(line)).length;
413
+ const fencedBlockCount = lines.filter((line) => /^```/.test(line)).length;
414
+ return trimmed.length + headingCount * 500 + checklistCount * 120 + fencedBlockCount * 80 + Math.min(lines.length, 200) * 10;
415
+ }
416
+
417
+ function shouldPreserveExistingTaskMarkdown(existing: string, incoming: string): boolean {
418
+ const existingTrimmed = existing.trim();
419
+ const incomingTrimmed = incoming.trim();
420
+ if (!existingTrimmed || !incomingTrimmed) return false;
421
+ if (existingTrimmed === incomingTrimmed) return true;
422
+
423
+ const existingScore = markdownPreservationScore(existingTrimmed);
424
+ const incomingScore = markdownPreservationScore(incomingTrimmed);
425
+ const incomingLooksLikeShortPrompt = incomingTrimmed.length < 500 && !/^#{1,6}\s+/m.test(incomingTrimmed);
426
+
427
+ return existingScore > incomingScore * 1.5 || incomingLooksLikeShortPrompt;
428
+ }
429
+
430
+ async function writeTaskMarkdownSafely(filePath: string, incomingMarkdown: string): Promise<boolean> {
431
+ const incoming = repairTaskMarkdown(incomingMarkdown).trimEnd() + "\n";
432
+ const existing = await readFile(filePath, "utf8").catch(() => "");
433
+ if (existing && shouldPreserveExistingTaskMarkdown(existing, incoming)) {
434
+ return false;
435
+ }
436
+ await writeFile(filePath, incoming, "utf8");
437
+ return true;
438
+ }
439
+
407
440
  async function writeTaskArtifact(paths: ReturnType<typeof getSessionPaths>, state: OrchestratorSessionState, task: OrchestratorTask) {
408
441
  const targetPath = path.join(getTaskFolder(paths, task.status), getTaskFileName(task));
409
442
  const existingPath = await findExistingTaskFile(paths, task);
@@ -945,7 +978,7 @@ ${stateJson}`
945
978
  for (const task of nextState.tasks) {
946
979
  const authored = (params.tasks as IncomingTask[] | undefined)?.find((input) => (input.id ?? task.id) === task.id)?.taskMarkdown;
947
980
  if (authored?.trim()) {
948
- await writeFile(path.join(getTaskFolder(paths, task.status), getTaskFileName(task)), authored.trimEnd() + "\n", "utf8");
981
+ await writeTaskMarkdownSafely(path.join(getTaskFolder(paths, task.status), getTaskFileName(task)), authored);
949
982
  }
950
983
  }
951
984
  state.activeSessionId = nextState.sessionId;
@@ -983,7 +1016,7 @@ ${stateJson}`
983
1016
  for (const task of nextState.tasks) {
984
1017
  const authored = (params.tasks as IncomingTask[] | undefined)?.find((input) => (input.id ?? task.id) === task.id)?.taskMarkdown;
985
1018
  if (authored?.trim()) {
986
- await writeFile(path.join(getTaskFolder(paths, task.status), getTaskFileName(task)), authored.trimEnd() + "\n", "utf8");
1019
+ await writeTaskMarkdownSafely(path.join(getTaskFolder(paths, task.status), getTaskFileName(task)), authored);
987
1020
  }
988
1021
  }
989
1022
  state.activeSessionId = nextState.sessionId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takomi",
3
- "version": "2.1.15",
3
+ "version": "2.1.16",
4
4
  "description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
5
5
  "type": "module",
6
6
  "bin": {