@gajae-code/coding-agent 0.12.5 → 0.12.7

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 (91) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +36 -1
  4. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  6. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  7. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  8. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  9. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  10. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  11. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  12. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  13. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  14. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  15. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
  16. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  17. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  18. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  19. package/dist/types/sdk/broker/broker.d.ts +18 -0
  20. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  21. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  22. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
  23. package/dist/types/sdk/bus/index.d.ts +4 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  26. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  27. package/dist/types/session/agent-session.d.ts +5 -4
  28. package/dist/types/session/attempt-record-store.d.ts +19 -0
  29. package/dist/types/session/session-manager.d.ts +1 -0
  30. package/dist/types/session/session-storage.d.ts +8 -0
  31. package/dist/types/task/receipt.d.ts +1 -0
  32. package/dist/types/task/types.d.ts +43 -0
  33. package/dist/types/tools/subagent.d.ts +15 -0
  34. package/dist/types/tools/todo-write.d.ts +3 -0
  35. package/package.json +8 -8
  36. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  37. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  38. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  39. package/src/async/job-manager.ts +430 -46
  40. package/src/config/model-profiles.ts +8 -0
  41. package/src/config/settings-schema.ts +17 -3
  42. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
  43. package/src/extensibility/extensions/runner.ts +80 -6
  44. package/src/extensibility/extensions/wrapper.ts +22 -15
  45. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  46. package/src/gjc-runtime/launch-tmux.ts +309 -293
  47. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  48. package/src/gjc-runtime/psmux-detect.ts +52 -6
  49. package/src/gjc-runtime/team-launch.ts +50 -2
  50. package/src/gjc-runtime/team-runtime.ts +716 -251
  51. package/src/gjc-runtime/team-store.ts +25 -7
  52. package/src/gjc-runtime/team-workers.ts +161 -1
  53. package/src/gjc-runtime/tmux-common.ts +16 -4
  54. package/src/gjc-runtime/tmux-gc.ts +37 -5
  55. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  56. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  57. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  58. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  59. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  60. package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
  61. package/src/internal-urls/docs-index.generated.ts +4 -4
  62. package/src/internal-urls/local-protocol.ts +39 -18
  63. package/src/modes/acp/acp-agent.ts +27 -13
  64. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  65. package/src/modes/components/plugin-settings.ts +5 -0
  66. package/src/modes/components/settings-selector.ts +4 -0
  67. package/src/modes/controllers/selector-controller.ts +1 -0
  68. package/src/prompts/tools/monitor.md +4 -4
  69. package/src/sdk/broker/broker.ts +93 -10
  70. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  71. package/src/sdk/broker/lifecycle.ts +691 -92
  72. package/src/sdk/bus/chat-daemon-control.ts +30 -6
  73. package/src/sdk/bus/index.ts +23 -6
  74. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  75. package/src/sdk/bus/telegram-daemon.ts +283 -44
  76. package/src/sdk/bus/topic-registry.ts +133 -10
  77. package/src/sdk/session.ts +7 -6
  78. package/src/session/agent-session.ts +752 -339
  79. package/src/session/attempt-record-store.ts +106 -0
  80. package/src/session/internal/managed-session-scope.ts +252 -67
  81. package/src/session/internal/managed-session-storage.ts +1 -0
  82. package/src/session/session-manager.ts +7 -0
  83. package/src/session/session-storage.ts +146 -24
  84. package/src/task/index.ts +230 -19
  85. package/src/task/provider-retry-status.ts +4 -1
  86. package/src/task/receipt.ts +2 -0
  87. package/src/task/types.ts +10 -0
  88. package/src/tools/monitor.ts +7 -1
  89. package/src/tools/output-meta.ts +10 -2
  90. package/src/tools/subagent.ts +97 -75
  91. package/src/tools/todo-write.ts +47 -0
@@ -32,6 +32,15 @@ const subagentSchema = z.object({
32
32
  id: z.string().optional().describe("single subagent id or backing job id for resume/steer"),
33
33
  message: z.string().optional().describe("message to deliver when resuming or steering a subagent"),
34
34
  pause: z.boolean().optional().describe("pause after steering a currently running subagent"),
35
+ condition: z
36
+ .enum(["all_terminal", "any_terminal"])
37
+ .optional()
38
+ .describe("terminal wait condition; defaults to all_terminal"),
39
+ heartbeat_ms: z
40
+ .number()
41
+ .refine(value => value === 0 || (Number.isInteger(value) && value >= 100 && value <= 5000))
42
+ .optional()
43
+ .describe("heartbeat interval; 0 disables"),
35
44
  timeout_ms: z.number().min(0).max(MAX_AWAIT_TIMEOUT_MS).optional().describe("await timeout in milliseconds"),
36
45
  limit: z.number().min(1).max(MAX_LIST_LIMIT).optional().describe("maximum subagents to return"),
37
46
  verbosity: z
@@ -97,6 +106,11 @@ export interface SubagentToolDetails {
97
106
  subagents: SubagentSnapshot[];
98
107
  /** Await outcome for a live await receipt; omitted when no wait was started. */
99
108
  awaitOutcome?: SubagentAwaitOutcome;
109
+ waitOutcome?: "completed" | "timed_out_wait" | "interrupted";
110
+ condition?: "all_terminal" | "any_terminal";
111
+ heartbeatMs?: number;
112
+ terminalIds?: string[];
113
+ acknowledgedTerminalIds?: string[];
100
114
  /** True only when the parent await was interrupted; the child was not cancelled. */
101
115
  interrupted?: true;
102
116
  }
@@ -344,35 +358,53 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
344
358
  signal: AbortSignal | undefined,
345
359
  onUpdate: AgentToolUpdateCallback<SubagentToolDetails> | undefined,
346
360
  ): Promise<AgentToolResult<SubagentToolDetails>> {
347
- const records = params.ids?.length
348
- ? this.#visibleRecordsByIds(manager, params.ids, ownerFilter)
361
+ const ids = params.ids
362
+ ?.map(id => id.trim())
363
+ .filter(Boolean)
364
+ .filter((id, index, all) => all.indexOf(id) === index);
365
+ const records = ids?.length
366
+ ? this.#visibleRecordsByIds(manager, ids, ownerFilter)
349
367
  : this.#runningRecords(manager, ownerFilter);
350
- const notFoundIds = this.#notFoundRecordIds(manager, params.ids ?? [], ownerFilter);
351
- if (records.length === 0) {
352
- const missing = notFoundIds.map(id =>
353
- this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."),
368
+ const notFoundIds = (ids ?? []).filter(id => !this.#findVisibleRecord(manager, id, ownerFilter));
369
+ if (records.length === 0)
370
+ return this.#buildSnapshotResult(
371
+ notFoundIds.map(id =>
372
+ this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."),
373
+ ),
374
+ "Subagent await",
354
375
  );
355
- return this.#buildSnapshotResult(missing, "Subagent await");
356
- }
357
-
358
- const runningJobs = records
359
- .filter(record => record.status === "running" && record.currentJobId)
360
- .map(record => manager.getJob(record.currentJobId!))
361
- .filter((job): job is AsyncJob => job !== undefined);
362
- if (runningJobs.length === 0) {
376
+ const targets = records
377
+ .map(record => manager.resolveSubagentWaitTarget(record.subagentId, ownerFilter))
378
+ .filter((target): target is NonNullable<typeof target> => target !== undefined);
379
+ if (targets.length === 0) return this.#buildSnapshotResult([], "Subagent await");
380
+ const condition = params.condition ?? "all_terminal";
381
+ const handle = manager.subscribeTerminalWait(targets, condition);
382
+ const timeoutMs = Math.min(
383
+ MAX_AWAIT_TIMEOUT_MS,
384
+ Math.max(0, Math.floor(params.timeout_ms ?? DEFAULT_AWAIT_TIMEOUT_MS)),
385
+ );
386
+ const targetsAlreadyTerminal = targets.every(
387
+ target =>
388
+ target.initialStatus === "completed" ||
389
+ target.initialStatus === "failed" ||
390
+ target.initialStatus === "cancelled",
391
+ );
392
+ if (targetsAlreadyTerminal) {
393
+ const waitResult = await handle.result;
394
+ handle.acknowledge(waitResult.terminalJobIds);
395
+ handle.close();
396
+ const terminalJobIds = waitResult.terminalJobIds;
363
397
  return await this.#buildRecordResult(manager, records, {
364
398
  title: "Subagent await",
365
- notFoundIds,
366
399
  verbosity: params.verbosity ?? "receipt",
400
+ waitOutcome: "completed",
401
+ condition,
402
+ terminalIds: terminalJobIds,
367
403
  });
368
404
  }
369
-
370
- const timeoutMs = Math.min(
371
- MAX_AWAIT_TIMEOUT_MS,
372
- Math.max(0, Math.floor(params.timeout_ms ?? DEFAULT_AWAIT_TIMEOUT_MS)),
373
- );
374
- const watchedJobIds = runningJobs.map(job => job.id);
405
+ const watchedJobIds = targets.map(target => target.jobId).filter((jobId): jobId is string => jobId !== null);
375
406
  manager.watchJobs(watchedJobIds);
407
+ const heartbeatMs = params.heartbeat_ms === undefined ? 500 : params.heartbeat_ms;
376
408
  let lastEmittedSignature: string | undefined;
377
409
  const emitIfChanged = (force: boolean): void => {
378
410
  if (!onUpdate) return;
@@ -382,57 +414,50 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
382
414
  lastEmittedSignature = signature;
383
415
  onUpdate(result);
384
416
  };
385
- const progressTimer = onUpdate ? setInterval(() => emitIfChanged(false), 500) : undefined;
386
- // Initial emission so the panel appears immediately; later idle ticks are
387
- // gated on a value-based rendered-state signature so unchanged progress no
388
- // longer rebuilds the renderer component or mutates transcript lines above
389
- // the viewport (the source of the await-panel repaint storms).
417
+ const progressTimer =
418
+ onUpdate && heartbeatMs > 0 ? setInterval(() => emitIfChanged(false), heartbeatMs) : undefined;
390
419
  emitIfChanged(true);
391
-
392
- let awaitOutcome: SubagentAwaitOutcome;
393
- const { promise: timeoutPromise, resolve: resolveTimeout } = Promise.withResolvers<SubagentAwaitOutcome>();
394
- const timeoutTimer = setTimeout(() => resolveTimeout("timed_out"), timeoutMs);
395
- const completionPromise = Promise.all(runningJobs.map(job => job.promise)).then(() => "completed" as const);
420
+ let waitOutcome: "completed" | "timed_out_wait" | "interrupted";
396
421
  let onAbort: (() => void) | undefined;
397
422
  try {
398
- if (signal) {
399
- const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers<SubagentAwaitOutcome>();
400
- onAbort = () => resolveAbort("interrupted");
401
- if (signal.aborted) onAbort();
402
- else signal.addEventListener("abort", onAbort, { once: true });
403
- awaitOutcome = await Promise.race([completionPromise, timeoutPromise, abortPromise]);
404
- } else {
405
- awaitOutcome = await Promise.race([completionPromise, timeoutPromise]);
406
- }
423
+ const abortPromise = new Promise<"interrupted">(resolve => {
424
+ onAbort = () => resolve("interrupted");
425
+ if (signal?.aborted) onAbort();
426
+ else signal?.addEventListener("abort", onAbort, { once: true });
427
+ });
428
+ waitOutcome =
429
+ targetsAlreadyTerminal && !signal?.aborted
430
+ ? "completed"
431
+ : await Promise.race([
432
+ handle.result.then(() => "completed" as const),
433
+ Bun.sleep(timeoutMs).then(() => "timed_out_wait" as const),
434
+ abortPromise,
435
+ ]);
407
436
  } finally {
408
- clearTimeout(timeoutTimer);
409
437
  if (signal && onAbort) signal.removeEventListener("abort", onAbort);
410
438
  if (progressTimer) clearInterval(progressTimer);
411
439
  }
412
- if (awaitOutcome !== "interrupted") {
413
- const terminalWatchedJobIds = watchedJobIds.filter(jobId => {
414
- const status = manager.getJob(jobId)?.status;
415
- return status === "completed" || status === "failed" || status === "cancelled";
416
- });
417
- manager.acknowledgeDeliveries(terminalWatchedJobIds);
418
- }
440
+ const waitResult = waitOutcome === "completed" ? await handle.result : undefined;
441
+ if (waitOutcome === "completed") handle.acknowledge(waitResult?.terminalJobIds);
419
442
  manager.unwatchJobs(watchedJobIds);
420
-
443
+ handle.close();
444
+ const awaitOutcome: SubagentAwaitOutcome =
445
+ waitOutcome === "completed" ? "completed" : waitOutcome === "timed_out_wait" ? "timed_out" : "interrupted";
421
446
  const finalRecords = this.#visibleRecordsByIds(
422
447
  manager,
423
448
  records.map(record => record.subagentId),
424
449
  ownerFilter,
425
450
  );
426
- const finalNotFoundIds = [...new Set([...notFoundIds, ...records.map(record => record.subagentId)])].filter(
427
- id => !finalRecords.some(record => record.subagentId === id),
428
- );
429
451
  return await this.#buildRecordResult(manager, finalRecords, {
430
- title: awaitOutcome === "interrupted" ? "Subagent await interrupted" : "Subagent await",
431
- notFoundIds: finalNotFoundIds,
432
- timedOut: awaitOutcome === "timed_out",
452
+ title: waitOutcome === "interrupted" ? "Subagent await interrupted" : "Subagent await",
453
+ notFoundIds,
454
+ timedOut: waitOutcome === "timed_out_wait",
433
455
  verbosity: params.verbosity ?? "receipt",
434
456
  attachLiveProgress: true,
435
457
  awaitOutcome,
458
+ waitOutcome,
459
+ condition,
460
+ terminalIds: waitResult?.terminalJobIds,
436
461
  });
437
462
  }
438
463
 
@@ -490,7 +515,8 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
490
515
  if (direct && isSubagentJob(direct) && (!ownerId || direct.ownerId === ownerId)) return direct;
491
516
  return manager
492
517
  .getAllJobs(ownerId ? { ownerId } : undefined)
493
- .find(job => isSubagentJob(job) && job.metadata?.subagent?.id === id);
518
+ .filter(job => isSubagentJob(job) && job.metadata?.subagent?.id === id)
519
+ .sort((a, b) => b.startTime - a.startTime)[0];
494
520
  }
495
521
 
496
522
  #visibleRecordsByIds(
@@ -551,6 +577,9 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
551
577
  verbosity?: SubagentParams["verbosity"];
552
578
  attachLiveProgress?: boolean;
553
579
  awaitOutcome?: SubagentAwaitOutcome;
580
+ waitOutcome?: "completed" | "timed_out_wait" | "interrupted";
581
+ condition?: "all_terminal" | "any_terminal";
582
+ terminalIds?: string[];
554
583
  },
555
584
  ): Promise<AgentToolResult<SubagentToolDetails>> {
556
585
  const verifiedOutputIds = await this.#verifiedOutputIds(records);
@@ -565,33 +594,28 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
565
594
  for (const id of options.notFoundIds ?? []) {
566
595
  snapshots.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
567
596
  }
568
- if (options.awaitOutcome !== "interrupted") {
569
- manager.acknowledgeDeliveries(
570
- snapshots
571
- .filter(
572
- s =>
573
- s.status !== "running" &&
574
- s.status !== "paused" &&
575
- s.status !== "queued" &&
576
- s.status !== "not_found",
577
- )
578
- .map(s => s.jobId),
579
- );
580
- }
581
597
  if (options.awaitOutcome === "interrupted") {
582
598
  for (const snapshot of snapshots) {
583
- if (snapshot.status === "running" || snapshot.status === "paused" || snapshot.status === "queued") {
599
+ if (snapshot.status === "running" || snapshot.status === "paused" || snapshot.status === "queued")
584
600
  snapshot.guidance = AWAIT_INTERRUPTED_GUIDANCE;
585
- }
586
601
  }
587
602
  }
588
- return this.#buildSnapshotResult(snapshots, options.title, options.awaitOutcome);
603
+ const details: SubagentToolDetails = {
604
+ subagents: snapshots,
605
+ ...(options.awaitOutcome !== undefined ? { awaitOutcome: options.awaitOutcome } : {}),
606
+ ...(options.waitOutcome !== undefined ? { waitOutcome: options.waitOutcome } : {}),
607
+ ...(options.condition !== undefined ? { condition: options.condition } : {}),
608
+ ...(options.terminalIds !== undefined ? { terminalIds: options.terminalIds } : {}),
609
+ };
610
+ if (options.awaitOutcome === "interrupted") details.interrupted = true;
611
+ return this.#buildSnapshotResult(snapshots, options.title, options.awaitOutcome, details);
589
612
  }
590
613
 
591
614
  #buildSnapshotResult(
592
615
  snapshots: SubagentSnapshot[],
593
616
  title: string,
594
617
  awaitOutcome?: SubagentAwaitOutcome,
618
+ extraDetails?: SubagentToolDetails,
595
619
  ): AgentToolResult<SubagentToolDetails> {
596
620
  const lines = [`## ${title} (${snapshots.length})`, ""];
597
621
  for (const snapshot of snapshots) {
@@ -623,7 +647,7 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
623
647
  }
624
648
  return {
625
649
  content: [{ type: "text", text: lines.join("\n").trimEnd() }],
626
- details: {
650
+ details: extraDetails ?? {
627
651
  subagents: snapshots,
628
652
  ...(awaitOutcome ? { awaitOutcome } : {}),
629
653
  ...(awaitOutcome === "interrupted" ? { interrupted: true } : {}),
@@ -651,8 +675,6 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
651
675
  ): Pick<SubagentSnapshot, "progress" | "liveProgressAvailable"> {
652
676
  if (!attachLiveProgress) return {};
653
677
  const liveProgressAvailable = manager.hasLiveSubagent(record.subagentId);
654
- // Only surface progress when a live producer exists; stale/retained progress
655
- // for a record with no live producer must degrade to a static snapshot (AC5).
656
678
  if (!liveProgressAvailable) return { liveProgressAvailable: false };
657
679
  const progress = manager.getSubagentProgress(record.subagentId);
658
680
  return {
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
+ import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
2
3
  import type { Component } from "@gajae-code/tui";
3
4
  import { Text } from "@gajae-code/tui";
4
5
  import { prompt } from "@gajae-code/utils";
@@ -62,6 +63,39 @@ const TodoOpEntry = z.object({
62
63
  text: z.string().optional().describe("note text"),
63
64
  });
64
65
 
66
+ const TODO_WRITE_KEYS = new Set(["ops"]);
67
+ const TODO_OP_KEYS = new Set(["op", "list", "task", "phase", "items", "text"]);
68
+ const TODO_INIT_ENTRY_KEYS = new Set(["phase", "items"]);
69
+
70
+ function hasUnknownKeys(value: object, allowed: Set<string>): boolean {
71
+ return Object.keys(value).some(key => !allowed.has(key));
72
+ }
73
+
74
+ function validateRawTodoArguments(arguments_: Record<string, unknown>): RawArgumentValidationResult {
75
+ if (hasUnknownKeys(arguments_, TODO_WRITE_KEYS)) return { outcome: "reject" };
76
+ if (!Array.isArray(arguments_.ops)) return { outcome: "passthrough" };
77
+ for (const entry of arguments_.ops) {
78
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
79
+ if (hasUnknownKeys(entry, TODO_OP_KEYS)) return { outcome: "reject" };
80
+ const record = entry as Record<string, unknown>;
81
+ if ((record.op === "done" || record.op === "drop") && !record.task && !record.phase) {
82
+ return { outcome: "reject" };
83
+ }
84
+ const list = record.list;
85
+ if (!Array.isArray(list)) continue;
86
+ for (const item of list) {
87
+ if (
88
+ typeof item === "object" &&
89
+ item !== null &&
90
+ !Array.isArray(item) &&
91
+ hasUnknownKeys(item, TODO_INIT_ENTRY_KEYS)
92
+ )
93
+ return { outcome: "reject" };
94
+ }
95
+ }
96
+ return { outcome: "passthrough" };
97
+ }
98
+
65
99
  const todoWriteSchema = z
66
100
  .object({
67
101
  ops: z.array(TodoOpEntry).min(1).describe("ordered todo operations"),
@@ -497,6 +531,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
497
531
  readonly summary = "Write a structured todo list to track progress within a session";
498
532
  readonly description: string;
499
533
  readonly parameters = todoWriteSchema;
534
+ readonly rawArgumentValidation = validateRawTodoArguments;
500
535
  readonly concurrency = "exclusive";
501
536
  readonly strict = true;
502
537
  readonly loadMode = "discoverable";
@@ -512,6 +547,18 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
512
547
  _context?: AgentToolContext,
513
548
  ): Promise<AgentToolResult<TodoWriteToolDetails>> {
514
549
  const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
550
+ const missingTarget = params.ops.find(
551
+ entry => (entry.op === "done" || entry.op === "drop") && !entry.task && !entry.phase,
552
+ );
553
+ if (missingTarget) {
554
+ const storage = this.session.getSessionFile() ? "session" : "memory";
555
+ const errors = [`Missing task or phase for ${missingTarget.op} operation`];
556
+ return {
557
+ content: [{ type: "text", text: formatSummary(previousPhases, errors) }],
558
+ details: { phases: previousPhases, storage },
559
+ isError: true,
560
+ };
561
+ }
515
562
  const { phases: updated, errors } = applyParams(previousPhases, params);
516
563
  this.session.setTodoPhases?.(updated);
517
564
  const storage = this.session.getSessionFile() ? "session" : "memory";