teapot-coding-agent 0.14.6 → 0.14.8

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.
@@ -493,6 +493,54 @@ export class Agent {
493
493
  await fs.writeFile(this.todoFile, text, "utf8");
494
494
  await this.log.append("todo", this.currentSession, this.currentBranch, { event: "set", by });
495
495
  }
496
+ /**
497
+ * Update SPECIFIC checkbox items without rewriting the whole list — the
498
+ * full-replacement path made agents lazy (a mid-task progress tick meant
499
+ * re-emitting the entire markdown, so they skipped updates) and risky (one
500
+ * sloppy regeneration mangled unrelated sections). Items are matched by
501
+ * normalized text; unknown items are reported back instead of silently
502
+ * dropped. Returns a short human-readable summary for the tool result.
503
+ */
504
+ async updateTodoItems(updates, by = "agent") {
505
+ const norm = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
506
+ const lines = this.todo.split("\n");
507
+ let matched = 0;
508
+ const unmatched = [];
509
+ for (const u of updates) {
510
+ const target = norm(u.item);
511
+ // find the LAST matching checkbox line (later duplicates win)
512
+ let idx = -1;
513
+ for (let i = lines.length - 1; i >= 0; i--) {
514
+ const m = lines[i].match(/^(\s*[-*+] \[)([ xX])(\].*)$/);
515
+ if (!m)
516
+ continue;
517
+ if (norm(m[3].slice(1)) === target) {
518
+ idx = i;
519
+ break;
520
+ }
521
+ }
522
+ if (idx === -1) {
523
+ unmatched.push(u.item);
524
+ continue;
525
+ }
526
+ lines[idx] = lines[idx].replace(/^(.* \[)[ xX](\].*)$/, (_s, a, b) => `${a}${u.done ? "x" : " "}${b}`);
527
+ matched++;
528
+ }
529
+ if (matched > 0) {
530
+ this.todo = lines.join("\n");
531
+ await fs.writeFile(this.todoFile, this.todo, "utf8");
532
+ await this.log.append("todo", this.currentSession, this.currentBranch, {
533
+ event: "items",
534
+ by,
535
+ matched,
536
+ ...(unmatched.length ? { unknown: unmatched } : {}),
537
+ });
538
+ }
539
+ const parts = [`${matched} item(s) updated`];
540
+ if (unmatched.length)
541
+ parts.push(`NOT FOUND in todo.md: ${unmatched.map((s) => `"${s}"`).join(", ")} — check get_todo and retry with the exact wording`);
542
+ return parts.join(". ");
543
+ }
496
544
  async setGoalStatus(status) {
497
545
  this.goal = { ...this.goal, status, updatedAt: new Date().toISOString() };
498
546
  await this.writeGoalFile();
@@ -595,15 +643,22 @@ export class Agent {
595
643
  });
596
644
  }
597
645
  setStatus(s, reason = "") {
598
- if (this.status !== s) {
646
+ // Update the field FIRST. log.append fires onEvent synchronously, and the
647
+ // master's child-event hook wakes parked wait_children callers — those
648
+ // re-check agent.status at that instant. Updating after the append made
649
+ // every waiter observe the OLD status ("running"), conclude "someone is
650
+ // still working", and stay parked until timeout even though the child had
651
+ // just settled.
652
+ const prev = this.status;
653
+ this.status = s;
654
+ this.statusReason = reason;
655
+ if (prev !== s) {
599
656
  void this.log.append("state", this.currentSession, this.currentBranch, {
600
- from: this.status,
657
+ from: prev,
601
658
  to: s,
602
659
  reason,
603
660
  });
604
661
  }
605
- this.status = s;
606
- this.statusReason = reason;
607
662
  bus.emit("update", { kind: "agent-update", agentId: this.opts.id });
608
663
  }
609
664
  /** Main auto-run loop: turn -> tools -> turn ... -> done/idle/stop. */
@@ -849,9 +904,28 @@ export class Agent {
849
904
  }
850
905
  if (call.function.name === "set_todo") {
851
906
  const a = safeParse(call.function.arguments);
907
+ // Preferred: surgical checkbox updates — no full rewrite, no risk
908
+ // of mangling unrelated sections, cheap enough to do mid-task.
909
+ const updates = Array.isArray(a.updates)
910
+ ? a.updates
911
+ .map((u) => {
912
+ const o = (u ?? {});
913
+ return { item: String(o.item ?? ""), done: o.done !== false };
914
+ })
915
+ .filter((u) => u.item.trim())
916
+ : [];
917
+ if (updates.length > 0) {
918
+ const note = await this.updateTodoItems(updates, "agent");
919
+ await this.answerMeta(call, `${note} (visible to the operator)`);
920
+ continue;
921
+ }
852
922
  const content = String(a.content ?? "").slice(0, 32_000);
923
+ if (!content.trim()) {
924
+ await this.answerMeta(call, "nothing to do: pass updates:[{item,done}] to check items off, or content to replace the whole list");
925
+ continue;
926
+ }
853
927
  await this.setTodo(content, "agent");
854
- await this.answerMeta(call, "task list updated (visible to the operator)");
928
+ await this.answerMeta(call, "task list replaced (visible to the operator)");
855
929
  continue;
856
930
  }
857
931
  if (call.function.name === "get_feedback") {
@@ -1443,11 +1517,29 @@ function allToolSpecs() {
1443
1517
  type: "function",
1444
1518
  function: {
1445
1519
  name: "set_todo",
1446
- description: "Replace the operator-visible task list (todo.md) — e.g. check off finished items or restate what remains. Keep it terse.",
1520
+ description: "Update the operator-visible task list (todo.md). PREFERRED: pass updates to check specific " +
1521
+ "items off as you finish them — cheap, surgical, no risk of mangling other sections. Only pass " +
1522
+ "content to rewrite the whole list when restructuring it. Call this promptly on every item you complete.",
1447
1523
  parameters: {
1448
1524
  type: "object",
1449
- properties: { content: { type: "string" } },
1450
- required: ["content"],
1525
+ properties: {
1526
+ updates: {
1527
+ type: "array",
1528
+ description: "surgical checkbox flips — match items by their exact text in todo.md",
1529
+ items: {
1530
+ type: "object",
1531
+ properties: {
1532
+ item: { type: "string", description: "checkbox item text (without the \"- [ ]\" marker)" },
1533
+ done: { type: "boolean", description: "true = check off, false = reopen (default true)" },
1534
+ },
1535
+ required: ["item"],
1536
+ },
1537
+ },
1538
+ content: {
1539
+ type: "string",
1540
+ description: "full replacement markdown — only for restructures; prefer updates",
1541
+ },
1542
+ },
1451
1543
  },
1452
1544
  },
1453
1545
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.14.6",
3
+ "version": "0.14.8",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",