opencode-orchestrator 1.2.11 → 1.2.14

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.
@@ -5,3 +5,4 @@ export * from "./types/index.js";
5
5
  export * from "./interfaces/index.js";
6
6
  export { ConcurrencyController } from "./concurrency.js";
7
7
  export { ParallelAgentManager, parallelAgentManager } from "./manager.js";
8
+ export { taskWAL } from "./persistence/task-wal.js";
@@ -15,6 +15,7 @@ export declare class TaskPoller {
15
15
  private pruneExpiredTasks;
16
16
  private onTaskComplete?;
17
17
  private pollingInterval?;
18
+ private messageCache;
18
19
  constructor(client: OpencodeClient, store: TaskStore, concurrency: ConcurrencyController, notifyParentIfAllComplete: (parentSessionID: string) => Promise<void>, scheduleCleanup: (taskId: string) => void, pruneExpiredTasks: () => void, onTaskComplete?: ((task: ParallelTask) => void | Promise<void>) | undefined);
19
20
  start(): void;
20
21
  stop(): void;
@@ -0,0 +1,11 @@
1
+ export declare class CleanupScheduler {
2
+ private intervals;
3
+ private directory;
4
+ constructor(directory: string);
5
+ start(): void;
6
+ private schedule;
7
+ stop(): void;
8
+ compactWAL(): Promise<void>;
9
+ cleanDocs(): Promise<void>;
10
+ rotateHistory(): Promise<void>;
11
+ }
@@ -5,62 +5,18 @@
5
5
  *
6
6
  * The LLM creates and checks items in .opencode/verification-checklist.md
7
7
  * The hook system verifies all items are checked before allowing CONCLUDE.
8
- *
9
- * This approach:
10
- * 1. LLM discovers environment and creates appropriate checklist items
11
- * 2. LLM executes and marks items as complete
12
- * 3. Hook verifies all items are checked (hard gate)
13
8
  */
14
9
  import { type ChecklistCategory, type ChecklistItem, type ChecklistVerificationResult, type VerificationResult } from "../../shared/index.js";
15
10
  export type { ChecklistItem, ChecklistCategory, ChecklistVerificationResult, VerificationResult };
16
- /** Path to the verification checklist file (re-export for convenience) */
17
11
  export declare const CHECKLIST_FILE: ".opencode/verification-checklist.md";
18
- /**
19
- * Parse checklist from markdown content
20
- */
21
12
  export declare function parseChecklist(content: string): ChecklistItem[];
22
- /**
23
- * Read checklist from file
24
- */
25
13
  export declare function readChecklist(directory: string): ChecklistItem[];
26
- /**
27
- * Verify that all checklist items are complete
28
- */
29
14
  export declare function verifyChecklist(directory: string): ChecklistVerificationResult;
30
- /**
31
- * Quick check if checklist exists and has items
32
- */
33
15
  export declare function hasValidChecklist(directory: string): boolean;
34
- /**
35
- * Get checklist summary for display
36
- */
37
16
  export declare function getChecklistSummary(directory: string): string;
38
- /**
39
- * Build prompt for when checklist verification fails
40
- */
41
17
  export declare function buildChecklistFailurePrompt(result: ChecklistVerificationResult): string;
42
- /**
43
- * Build checklist creation prompt (for inclusion in agent prompts)
44
- */
45
18
  export declare function getChecklistCreationInstructions(): string;
46
- /**
47
- * Verify mission completion conditions
48
- *
49
- * Checks (in order):
50
- * 1. Verification checklist (primary - if exists)
51
- * 2. TODO completion rate (fallback)
52
- * 3. Sync issues (always checked)
53
- */
54
19
  export declare function verifyMissionCompletion(directory: string): VerificationResult;
55
- /**
56
- * Build prompt for when conclusion is rejected due to verification failure
57
- */
58
20
  export declare function buildVerificationFailurePrompt(result: VerificationResult): string;
59
- /**
60
- * Build prompt for when TODO is incomplete
61
- */
62
21
  export declare function buildTodoIncompletePrompt(result: VerificationResult): string;
63
- /**
64
- * Build a concise status summary for logs
65
- */
66
22
  export declare function buildVerificationSummary(result: VerificationResult): string;
@@ -13,12 +13,14 @@
13
13
  import type { PluginInput } from "@opencode-ai/plugin";
14
14
  import type { ConcurrencyController } from "../agents/concurrency.js";
15
15
  import { type TaskStatus, type TrackedTask, type TaskCompletionInfo } from "../../shared/index.js";
16
+ import type { TodoSyncService } from "../sync/todo-sync-service.js";
16
17
  export type { TaskStatus, TrackedTask, TaskCompletionInfo } from "../../shared/index.js";
17
18
  type OpencodeClient = PluginInput["client"];
18
19
  export declare class TaskToastManager {
19
20
  private tasks;
20
21
  private client;
21
22
  private concurrency;
23
+ private todoSync;
22
24
  /**
23
25
  * Initialize the manager with OpenCode client
24
26
  */
@@ -27,6 +29,10 @@ export declare class TaskToastManager {
27
29
  * Set concurrency controller (can be set after init)
28
30
  */
29
31
  setConcurrencyController(concurrency: ConcurrencyController): void;
32
+ /**
33
+ * Set TodoSyncService for TUI status synchronization
34
+ */
35
+ setTodoSync(service: TodoSyncService): void;
30
36
  /**
31
37
  * Add a new task and show consolidated toast
32
38
  */
@@ -0,0 +1,5 @@
1
+ import { type Todo } from "../../shared/index.js";
2
+ /**
3
+ * Parses markdown content into Todo objects
4
+ */
5
+ export declare function parseTodoMd(content: string): Todo[];
@@ -0,0 +1,35 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ type OpencodeClient = PluginInput["client"];
3
+ interface TrackedTaskTodo {
4
+ id: string;
5
+ description: string;
6
+ status: string;
7
+ agent: string;
8
+ isBackground: boolean;
9
+ parentSessionID?: string;
10
+ }
11
+ export declare class TodoSyncService {
12
+ private client;
13
+ private directory;
14
+ private todoPath;
15
+ private fileTodos;
16
+ private taskTodos;
17
+ private updateTimeout;
18
+ private watcher;
19
+ private activeSessions;
20
+ constructor(client: OpencodeClient, directory: string);
21
+ start(): Promise<void>;
22
+ registerSession(sessionID: string): void;
23
+ unregisterSession(sessionID: string): void;
24
+ private reloadFileTodos;
25
+ /**
26
+ * Called by TaskToastManager when tasks change
27
+ */
28
+ updateTaskStatus(task: TrackedTaskTodo): void;
29
+ removeTask(taskId: string): void;
30
+ private broadcastUpdate;
31
+ private scheduleUpdate;
32
+ private sendTodosToSession;
33
+ stop(): void;
34
+ }
35
+ export {};
package/dist/index.js CHANGED
@@ -417,6 +417,10 @@ var init_memory_hooks = __esm({
417
417
  COMPLETED: "completed",
418
418
  PROGRESS: "progress",
419
419
  FAILED: "failed"
420
+ },
421
+ PREFIX: {
422
+ TASK: "task-",
423
+ FILE: "file-task-"
420
424
  }
421
425
  };
422
426
  }
@@ -5978,15 +5982,15 @@ function mergeDefs(...defs) {
5978
5982
  function cloneDef(schema) {
5979
5983
  return mergeDefs(schema._zod.def);
5980
5984
  }
5981
- function getElementAtPath(obj, path10) {
5982
- if (!path10)
5985
+ function getElementAtPath(obj, path12) {
5986
+ if (!path12)
5983
5987
  return obj;
5984
- return path10.reduce((acc, key) => acc?.[key], obj);
5988
+ return path12.reduce((acc, key) => acc?.[key], obj);
5985
5989
  }
5986
5990
  function promiseAllObject(promisesObj) {
5987
5991
  const keys = Object.keys(promisesObj);
5988
- const promises2 = keys.map((key) => promisesObj[key]);
5989
- return Promise.all(promises2).then((results) => {
5992
+ const promises4 = keys.map((key) => promisesObj[key]);
5993
+ return Promise.all(promises4).then((results) => {
5990
5994
  const resolvedObj = {};
5991
5995
  for (let i = 0; i < keys.length; i++) {
5992
5996
  resolvedObj[keys[i]] = results[i];
@@ -6342,11 +6346,11 @@ function aborted(x, startIndex = 0) {
6342
6346
  }
6343
6347
  return false;
6344
6348
  }
6345
- function prefixIssues(path10, issues) {
6349
+ function prefixIssues(path12, issues) {
6346
6350
  return issues.map((iss) => {
6347
6351
  var _a2;
6348
6352
  (_a2 = iss).path ?? (_a2.path = []);
6349
- iss.path.unshift(path10);
6353
+ iss.path.unshift(path12);
6350
6354
  return iss;
6351
6355
  });
6352
6356
  }
@@ -6514,7 +6518,7 @@ function treeifyError(error92, _mapper) {
6514
6518
  return issue3.message;
6515
6519
  };
6516
6520
  const result = { errors: [] };
6517
- const processError = (error93, path10 = []) => {
6521
+ const processError = (error93, path12 = []) => {
6518
6522
  var _a2, _b;
6519
6523
  for (const issue3 of error93.issues) {
6520
6524
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -6524,7 +6528,7 @@ function treeifyError(error92, _mapper) {
6524
6528
  } else if (issue3.code === "invalid_element") {
6525
6529
  processError({ issues: issue3.issues }, issue3.path);
6526
6530
  } else {
6527
- const fullpath = [...path10, ...issue3.path];
6531
+ const fullpath = [...path12, ...issue3.path];
6528
6532
  if (fullpath.length === 0) {
6529
6533
  result.errors.push(mapper(issue3));
6530
6534
  continue;
@@ -6556,8 +6560,8 @@ function treeifyError(error92, _mapper) {
6556
6560
  }
6557
6561
  function toDotPath(_path) {
6558
6562
  const segs = [];
6559
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6560
- for (const seg of path10) {
6563
+ const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
6564
+ for (const seg of path12) {
6561
6565
  if (typeof seg === "number")
6562
6566
  segs.push(`[${seg}]`);
6563
6567
  else if (typeof seg === "symbol")
@@ -18829,6 +18833,7 @@ var TaskToastManager = class {
18829
18833
  tasks = /* @__PURE__ */ new Map();
18830
18834
  client = null;
18831
18835
  concurrency = null;
18836
+ todoSync = null;
18832
18837
  /**
18833
18838
  * Initialize the manager with OpenCode client
18834
18839
  */
@@ -18842,6 +18847,12 @@ var TaskToastManager = class {
18842
18847
  setConcurrencyController(concurrency) {
18843
18848
  this.concurrency = concurrency;
18844
18849
  }
18850
+ /**
18851
+ * Set TodoSyncService for TUI status synchronization
18852
+ */
18853
+ setTodoSync(service) {
18854
+ this.todoSync = service;
18855
+ }
18845
18856
  /**
18846
18857
  * Add a new task and show consolidated toast
18847
18858
  */
@@ -18857,6 +18868,7 @@ var TaskToastManager = class {
18857
18868
  sessionID: task.sessionID
18858
18869
  };
18859
18870
  this.tasks.set(task.id, trackedTask);
18871
+ this.todoSync?.updateTaskStatus(trackedTask);
18860
18872
  this.showTaskListToast(trackedTask);
18861
18873
  }
18862
18874
  /**
@@ -18866,6 +18878,7 @@ var TaskToastManager = class {
18866
18878
  const task = this.tasks.get(id);
18867
18879
  if (task) {
18868
18880
  task.status = status;
18881
+ this.todoSync?.updateTaskStatus(task);
18869
18882
  }
18870
18883
  }
18871
18884
  /**
@@ -18873,6 +18886,7 @@ var TaskToastManager = class {
18873
18886
  */
18874
18887
  removeTask(id) {
18875
18888
  this.tasks.delete(id);
18889
+ this.todoSync?.removeTask(id);
18876
18890
  }
18877
18891
  /**
18878
18892
  * Get all running tasks (newest first)
@@ -20215,15 +20229,15 @@ function mergeDefs2(...defs) {
20215
20229
  function cloneDef2(schema) {
20216
20230
  return mergeDefs2(schema._zod.def);
20217
20231
  }
20218
- function getElementAtPath2(obj, path10) {
20219
- if (!path10)
20232
+ function getElementAtPath2(obj, path12) {
20233
+ if (!path12)
20220
20234
  return obj;
20221
- return path10.reduce((acc, key) => acc?.[key], obj);
20235
+ return path12.reduce((acc, key) => acc?.[key], obj);
20222
20236
  }
20223
20237
  function promiseAllObject2(promisesObj) {
20224
20238
  const keys = Object.keys(promisesObj);
20225
- const promises2 = keys.map((key) => promisesObj[key]);
20226
- return Promise.all(promises2).then((results) => {
20239
+ const promises4 = keys.map((key) => promisesObj[key]);
20240
+ return Promise.all(promises4).then((results) => {
20227
20241
  const resolvedObj = {};
20228
20242
  for (let i = 0; i < keys.length; i++) {
20229
20243
  resolvedObj[keys[i]] = results[i];
@@ -20601,11 +20615,11 @@ function aborted2(x, startIndex = 0) {
20601
20615
  }
20602
20616
  return false;
20603
20617
  }
20604
- function prefixIssues2(path10, issues) {
20618
+ function prefixIssues2(path12, issues) {
20605
20619
  return issues.map((iss) => {
20606
20620
  var _a2;
20607
20621
  (_a2 = iss).path ?? (_a2.path = []);
20608
- iss.path.unshift(path10);
20622
+ iss.path.unshift(path12);
20609
20623
  return iss;
20610
20624
  });
20611
20625
  }
@@ -20788,7 +20802,7 @@ function formatError2(error92, mapper = (issue3) => issue3.message) {
20788
20802
  }
20789
20803
  function treeifyError2(error92, mapper = (issue3) => issue3.message) {
20790
20804
  const result = { errors: [] };
20791
- const processError = (error93, path10 = []) => {
20805
+ const processError = (error93, path12 = []) => {
20792
20806
  var _a2, _b;
20793
20807
  for (const issue3 of error93.issues) {
20794
20808
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -20798,7 +20812,7 @@ function treeifyError2(error92, mapper = (issue3) => issue3.message) {
20798
20812
  } else if (issue3.code === "invalid_element") {
20799
20813
  processError({ issues: issue3.issues }, issue3.path);
20800
20814
  } else {
20801
- const fullpath = [...path10, ...issue3.path];
20815
+ const fullpath = [...path12, ...issue3.path];
20802
20816
  if (fullpath.length === 0) {
20803
20817
  result.errors.push(mapper(issue3));
20804
20818
  continue;
@@ -20830,8 +20844,8 @@ function treeifyError2(error92, mapper = (issue3) => issue3.message) {
20830
20844
  }
20831
20845
  function toDotPath2(_path) {
20832
20846
  const segs = [];
20833
- const path10 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
20834
- for (const seg of path10) {
20847
+ const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
20848
+ for (const seg of path12) {
20835
20849
  if (typeof seg === "number")
20836
20850
  segs.push(`[${seg}]`);
20837
20851
  else if (typeof seg === "symbol")
@@ -32808,13 +32822,13 @@ function resolveRef(ref, ctx) {
32808
32822
  if (!ref.startsWith("#")) {
32809
32823
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
32810
32824
  }
32811
- const path10 = ref.slice(1).split("/").filter(Boolean);
32812
- if (path10.length === 0) {
32825
+ const path12 = ref.slice(1).split("/").filter(Boolean);
32826
+ if (path12.length === 0) {
32813
32827
  return ctx.rootSchema;
32814
32828
  }
32815
32829
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
32816
- if (path10[0] === defsKey) {
32817
- const key = path10[1];
32830
+ if (path12[0] === defsKey) {
32831
+ const key = path12[1];
32818
32832
  if (!key || !ctx.defs[key]) {
32819
32833
  throw new Error(`Reference not found: ${ref}`);
32820
32834
  }
@@ -33625,6 +33639,7 @@ var TaskPoller = class {
33625
33639
  this.onTaskComplete = onTaskComplete;
33626
33640
  }
33627
33641
  pollingInterval;
33642
+ messageCache = /* @__PURE__ */ new Map();
33628
33643
  start() {
33629
33644
  if (this.pollingInterval) return;
33630
33645
  log("[task-poller.ts] start() - polling started");
@@ -33717,7 +33732,16 @@ var TaskPoller = class {
33717
33732
  }
33718
33733
  async updateTaskProgress(task) {
33719
33734
  try {
33735
+ const cached3 = this.messageCache.get(task.sessionID);
33736
+ const statusResult = await this.client.session.status();
33737
+ const sessionInfo = statusResult.data?.[task.sessionID];
33738
+ const currentMsgCount = sessionInfo?.messageCount ?? 0;
33739
+ if (cached3 && cached3.count === currentMsgCount) {
33740
+ task.stablePolls = (task.stablePolls ?? 0) + 1;
33741
+ return;
33742
+ }
33720
33743
  const result = await this.client.session.messages({ path: { id: task.sessionID } });
33744
+ this.messageCache.set(task.sessionID, { count: currentMsgCount, lastChecked: /* @__PURE__ */ new Date() });
33721
33745
  if (result.error) return;
33722
33746
  const messages = result.data ?? [];
33723
33747
  const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
@@ -33741,9 +33765,8 @@ var TaskPoller = class {
33741
33765
  lastMessage: lastMessage?.slice(0, 100),
33742
33766
  lastUpdate: /* @__PURE__ */ new Date()
33743
33767
  };
33744
- const currentMsgCount = messages.length;
33745
33768
  if (task.lastMsgCount === currentMsgCount) {
33746
- task.stablePolls = (task.stablePolls ?? 0) + 1;
33769
+ task.stablePolls = 0;
33747
33770
  } else {
33748
33771
  task.stablePolls = 0;
33749
33772
  }
@@ -37135,10 +37158,10 @@ async function resolveCommandPath(key, commandName) {
37135
37158
  const currentPending = pending.get(key);
37136
37159
  if (currentPending) return currentPending;
37137
37160
  const promise3 = (async () => {
37138
- const path10 = await findCommand(commandName);
37139
- cache[key] = path10;
37161
+ const path12 = await findCommand(commandName);
37162
+ cache[key] = path12;
37140
37163
  pending.delete(key);
37141
- return path10;
37164
+ return path12;
37142
37165
  })();
37143
37166
  pending.set(key, promise3);
37144
37167
  return promise3;
@@ -37147,21 +37170,21 @@ async function resolveCommandPath(key, commandName) {
37147
37170
  // src/core/notification/os-notify/notifier.ts
37148
37171
  var execAsync2 = promisify2(exec2);
37149
37172
  async function notifyDarwin(title, message) {
37150
- const path10 = await resolveCommandPath(
37173
+ const path12 = await resolveCommandPath(
37151
37174
  NOTIFICATION_COMMAND_KEYS.OSASCRIPT,
37152
37175
  NOTIFICATION_COMMANDS.OSASCRIPT
37153
37176
  );
37154
- if (!path10) return;
37177
+ if (!path12) return;
37155
37178
  const escT = title.replace(/"/g, '\\"');
37156
37179
  const escM = message.replace(/"/g, '\\"');
37157
- await execAsync2(`${path10} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
37180
+ await execAsync2(`${path12} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
37158
37181
  }
37159
37182
  async function notifyLinux(title, message) {
37160
- const path10 = await resolveCommandPath(
37183
+ const path12 = await resolveCommandPath(
37161
37184
  NOTIFICATION_COMMAND_KEYS.NOTIFY_SEND,
37162
37185
  NOTIFICATION_COMMANDS.NOTIFY_SEND
37163
37186
  );
37164
- if (path10) await execAsync2(`${path10} "${title}" "${message}" 2>/dev/null`);
37187
+ if (path12) await execAsync2(`${path12} "${title}" "${message}" 2>/dev/null`);
37165
37188
  }
37166
37189
  async function notifyWindows(title, message) {
37167
37190
  const ps = await resolveCommandPath(
@@ -37208,11 +37231,11 @@ init_os();
37208
37231
  async function playDarwin(soundPath) {
37209
37232
  if (!soundPath) return;
37210
37233
  try {
37211
- const path10 = await resolveCommandPath(
37234
+ const path12 = await resolveCommandPath(
37212
37235
  NOTIFICATION_COMMAND_KEYS.AFPLAY,
37213
37236
  NOTIFICATION_COMMANDS.AFPLAY
37214
37237
  );
37215
- if (path10) exec3(`"${path10}" "${soundPath}"`);
37238
+ if (path12) exec3(`"${path12}" "${soundPath}"`);
37216
37239
  } catch (err) {
37217
37240
  log(`[session-notify] Error playing sound (Darwin): ${err}`);
37218
37241
  }
@@ -38364,6 +38387,262 @@ var PluginManager = class _PluginManager {
38364
38387
  }
38365
38388
  };
38366
38389
 
38390
+ // src/core/sync/todo-sync-service.ts
38391
+ init_shared();
38392
+ import * as fs10 from "node:fs";
38393
+ import * as path9 from "node:path";
38394
+
38395
+ // src/core/sync/todo-parser.ts
38396
+ init_shared();
38397
+ function parseTodoMd(content) {
38398
+ const lines = content.split("\n");
38399
+ const todos = [];
38400
+ const generateId = (text, index2) => {
38401
+ return `${TODO_CONSTANTS.PREFIX.FILE}${index2}-${text.substring(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`;
38402
+ };
38403
+ let index = 0;
38404
+ for (const line of lines) {
38405
+ const match = line.match(/^\s*-\s*\[([ xX\/\-\.])\]\s*(.+)$/);
38406
+ if (match) {
38407
+ const [, statusChar, text] = match;
38408
+ const content2 = text.trim();
38409
+ let status = TODO_STATUS2.PENDING;
38410
+ switch (statusChar.toLowerCase()) {
38411
+ case "x":
38412
+ status = TODO_STATUS2.COMPLETED;
38413
+ break;
38414
+ case "/":
38415
+ case ".":
38416
+ status = TODO_STATUS2.IN_PROGRESS;
38417
+ break;
38418
+ case "-":
38419
+ status = TODO_STATUS2.CANCELLED;
38420
+ break;
38421
+ case " ":
38422
+ default:
38423
+ status = TODO_STATUS2.PENDING;
38424
+ break;
38425
+ }
38426
+ todos.push({
38427
+ id: generateId(content2, index),
38428
+ content: content2,
38429
+ status,
38430
+ priority: STATUS_LABEL.MEDIUM,
38431
+ // Default priority for file items
38432
+ createdAt: /* @__PURE__ */ new Date()
38433
+ });
38434
+ index++;
38435
+ }
38436
+ }
38437
+ return todos;
38438
+ }
38439
+
38440
+ // src/core/sync/todo-sync-service.ts
38441
+ var TodoSyncService = class {
38442
+ client;
38443
+ directory;
38444
+ todoPath;
38445
+ fileTodos = [];
38446
+ taskTodos = /* @__PURE__ */ new Map();
38447
+ updateTimeout = null;
38448
+ watcher = null;
38449
+ activeSessions = /* @__PURE__ */ new Set();
38450
+ constructor(client, directory) {
38451
+ this.client = client;
38452
+ this.directory = directory;
38453
+ this.todoPath = path9.join(this.directory, PATHS.TODO);
38454
+ }
38455
+ async start() {
38456
+ await this.reloadFileTodos();
38457
+ if (fs10.existsSync(this.todoPath)) {
38458
+ let timer;
38459
+ this.watcher = fs10.watch(this.todoPath, (eventType) => {
38460
+ if (eventType === "change" || eventType === "rename") {
38461
+ clearTimeout(timer);
38462
+ timer = setTimeout(() => {
38463
+ this.reloadFileTodos().catch((err) => log(`[TodoSync] Error reloading: ${err}`));
38464
+ }, 500);
38465
+ }
38466
+ });
38467
+ }
38468
+ }
38469
+ registerSession(sessionID) {
38470
+ this.activeSessions.add(sessionID);
38471
+ this.scheduleUpdate(sessionID);
38472
+ }
38473
+ unregisterSession(sessionID) {
38474
+ this.activeSessions.delete(sessionID);
38475
+ }
38476
+ async reloadFileTodos() {
38477
+ try {
38478
+ if (fs10.existsSync(this.todoPath)) {
38479
+ const content = await fs10.promises.readFile(this.todoPath, "utf-8");
38480
+ this.fileTodos = parseTodoMd(content);
38481
+ this.broadcastUpdate();
38482
+ }
38483
+ } catch (error92) {
38484
+ log(`[TodoSync] Failed to read todo.md: ${error92}`);
38485
+ }
38486
+ }
38487
+ /**
38488
+ * Called by TaskToastManager when tasks change
38489
+ */
38490
+ updateTaskStatus(task) {
38491
+ this.taskTodos.set(task.id, task);
38492
+ if (task.parentSessionID) {
38493
+ this.scheduleUpdate(task.parentSessionID);
38494
+ } else {
38495
+ this.broadcastUpdate();
38496
+ }
38497
+ }
38498
+ removeTask(taskId) {
38499
+ const task = this.taskTodos.get(taskId);
38500
+ if (task) {
38501
+ this.taskTodos.delete(taskId);
38502
+ if (task.parentSessionID) {
38503
+ this.scheduleUpdate(task.parentSessionID);
38504
+ } else {
38505
+ this.broadcastUpdate();
38506
+ }
38507
+ }
38508
+ }
38509
+ broadcastUpdate() {
38510
+ for (const sessionID of this.activeSessions) {
38511
+ this.scheduleUpdate(sessionID);
38512
+ }
38513
+ }
38514
+ scheduleUpdate(sessionID) {
38515
+ this.sendTodosToSession(sessionID).catch((err) => {
38516
+ });
38517
+ }
38518
+ async sendTodosToSession(sessionID) {
38519
+ const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
38520
+ let status = TODO_STATUS2.PENDING;
38521
+ const s = t.status.toLowerCase();
38522
+ if (s.includes(STATUS_LABEL.RUNNING) || s.includes("wait") || s.includes("que")) status = TODO_STATUS2.IN_PROGRESS;
38523
+ else if (s.includes(STATUS_LABEL.COMPLETED) || s.includes(STATUS_LABEL.DONE)) status = TODO_STATUS2.COMPLETED;
38524
+ else if (s.includes(STATUS_LABEL.FAILED) || s.includes(STATUS_LABEL.ERROR)) status = TODO_STATUS2.CANCELLED;
38525
+ else if (s.includes(STATUS_LABEL.CANCELLED)) status = TODO_STATUS2.CANCELLED;
38526
+ return {
38527
+ id: `${TODO_CONSTANTS.PREFIX.TASK}${t.id}`,
38528
+ // Prefix to avoid collision
38529
+ content: `[${t.agent.toUpperCase()}] ${t.description}`,
38530
+ status,
38531
+ priority: t.isBackground ? STATUS_LABEL.LOW : STATUS_LABEL.HIGH,
38532
+ createdAt: /* @__PURE__ */ new Date()
38533
+ };
38534
+ });
38535
+ const merged = [
38536
+ ...this.fileTodos,
38537
+ ...taskTodosList
38538
+ ];
38539
+ const payloadTodos = merged.map((todo) => ({
38540
+ id: todo.id,
38541
+ content: todo.content,
38542
+ status: todo.status,
38543
+ priority: todo.priority
38544
+ }));
38545
+ try {
38546
+ await this.client.session.todo({
38547
+ path: { id: sessionID },
38548
+ // Standardize to id
38549
+ body: { todos: payloadTodos }
38550
+ });
38551
+ } catch (error92) {
38552
+ }
38553
+ }
38554
+ stop() {
38555
+ if (this.watcher) {
38556
+ this.watcher.close();
38557
+ }
38558
+ }
38559
+ };
38560
+
38561
+ // src/core/cleanup/cleanup-scheduler.ts
38562
+ import * as fs11 from "node:fs";
38563
+ import * as path10 from "node:path";
38564
+ init_shared();
38565
+ var CleanupScheduler = class {
38566
+ intervals = /* @__PURE__ */ new Map();
38567
+ directory;
38568
+ constructor(directory) {
38569
+ this.directory = directory;
38570
+ }
38571
+ start() {
38572
+ this.schedule("wal-compact", () => this.compactWAL(), 10 * 60 * 1e3);
38573
+ this.schedule("docs-clean", () => this.cleanDocs(), 60 * 60 * 1e3);
38574
+ this.schedule("history-rotate", () => this.rotateHistory(), 24 * 60 * 60 * 1e3);
38575
+ log(`[Cleanup] Scheduler started`);
38576
+ }
38577
+ schedule(name, fn, intervalMs) {
38578
+ const timer = setInterval(() => {
38579
+ fn().catch((err) => log(`[Cleanup] ${name} failed:`, err));
38580
+ }, intervalMs);
38581
+ if (timer.unref) timer.unref();
38582
+ this.intervals.set(name, timer);
38583
+ }
38584
+ stop() {
38585
+ for (const timer of this.intervals.values()) {
38586
+ clearInterval(timer);
38587
+ }
38588
+ this.intervals.clear();
38589
+ log(`[Cleanup] Scheduler stopped`);
38590
+ }
38591
+ async compactWAL() {
38592
+ try {
38593
+ const manager = parallelAgentManager.getInstance();
38594
+ const activeTasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.RUNNING || t.status === TASK_STATUS.PENDING);
38595
+ await taskWAL.compact(activeTasks);
38596
+ } catch (error92) {
38597
+ log(`[Cleanup] Failed to compact WAL: ${error92}`);
38598
+ }
38599
+ }
38600
+ async cleanDocs() {
38601
+ try {
38602
+ const stats2 = await stats();
38603
+ if (stats2.totalSize > 10 * 1024 * 1024) {
38604
+ const allDocs = await list();
38605
+ allDocs.sort((a, b) => new Date(a.fetchedAt).getTime() - new Date(b.fetchedAt).getTime());
38606
+ const toDelete = allDocs.slice(0, Math.floor(allDocs.length / 2));
38607
+ for (const doc of toDelete) {
38608
+ await remove(doc.url);
38609
+ }
38610
+ log(`[Cleanup] Pruned ${toDelete.length} documents due to size limit`);
38611
+ }
38612
+ } catch (error92) {
38613
+ }
38614
+ }
38615
+ async rotateHistory() {
38616
+ try {
38617
+ const historyPath = path10.join(this.directory, ".opencode/archive/todo_history.jsonl");
38618
+ if (!fs11.existsSync(historyPath)) return;
38619
+ const stat = await fs11.promises.stat(historyPath);
38620
+ if (stat.size === 0) return;
38621
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
38622
+ const archivePath = path10.join(
38623
+ this.directory,
38624
+ `.opencode/archive/todo_history.${dateStr}.jsonl`
38625
+ );
38626
+ await fs11.promises.rename(historyPath, archivePath);
38627
+ await fs11.promises.writeFile(historyPath, "");
38628
+ const archiveDir = path10.dirname(historyPath);
38629
+ const files = await fs11.promises.readdir(archiveDir);
38630
+ const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1e3;
38631
+ for (const file3 of files) {
38632
+ if (file3.startsWith("todo_history.") && file3.endsWith(".jsonl")) {
38633
+ const filePath = path10.join(archiveDir, file3);
38634
+ const fStat = await fs11.promises.stat(filePath);
38635
+ if (fStat.mtimeMs < cutoff) {
38636
+ await fs11.promises.unlink(filePath);
38637
+ }
38638
+ }
38639
+ }
38640
+ } catch (error92) {
38641
+ log(`[Cleanup] History rotation error: ${error92}`);
38642
+ }
38643
+ }
38644
+ };
38645
+
38367
38646
  // src/plugin-handlers/tool-execute-pre-handler.ts
38368
38647
  function createToolExecuteBeforeHandler(ctx) {
38369
38648
  const { sessions, directory } = ctx;
@@ -38427,17 +38706,17 @@ function createChatMessageHandler(ctx) {
38427
38706
  init_shared();
38428
38707
 
38429
38708
  // src/utils/compatibility/claude.ts
38430
- import fs10 from "fs";
38431
- import path9 from "path";
38709
+ import fs12 from "fs";
38710
+ import path11 from "path";
38432
38711
  function findClaudeRules(startDir = process.cwd()) {
38433
38712
  try {
38434
38713
  let currentDir = startDir;
38435
- const root = path9.parse(startDir).root;
38714
+ const root = path11.parse(startDir).root;
38436
38715
  while (true) {
38437
- const claudeMdPath = path9.join(currentDir, "CLAUDE.md");
38438
- if (fs10.existsSync(claudeMdPath)) {
38716
+ const claudeMdPath = path11.join(currentDir, "CLAUDE.md");
38717
+ if (fs12.existsSync(claudeMdPath)) {
38439
38718
  try {
38440
- const content = fs10.readFileSync(claudeMdPath, "utf-8");
38719
+ const content = fs12.readFileSync(claudeMdPath, "utf-8");
38441
38720
  log(`[compatibility] Loaded CLAUDE.md from ${claudeMdPath}`);
38442
38721
  return formatRules("CLAUDE.md", content);
38443
38722
  } catch (e) {
@@ -38445,11 +38724,11 @@ function findClaudeRules(startDir = process.cwd()) {
38445
38724
  }
38446
38725
  }
38447
38726
  if (currentDir === root) break;
38448
- currentDir = path9.dirname(currentDir);
38727
+ currentDir = path11.dirname(currentDir);
38449
38728
  }
38450
- const copilotPath = path9.join(startDir, ".github", "copilot-instructions.md");
38451
- if (fs10.existsSync(copilotPath)) {
38452
- return formatRules("Copilot Instructions", fs10.readFileSync(copilotPath, "utf-8"));
38729
+ const copilotPath = path11.join(startDir, ".github", "copilot-instructions.md");
38730
+ if (fs12.existsSync(copilotPath)) {
38731
+ return formatRules("Copilot Instructions", fs12.readFileSync(copilotPath, "utf-8"));
38453
38732
  }
38454
38733
  return null;
38455
38734
  } catch (error92) {
@@ -39054,6 +39333,11 @@ var OrchestratorPlugin = async (input) => {
39054
39333
  await pluginManager.initialize(directory);
39055
39334
  const dynamicTools = pluginManager.getDynamicTools();
39056
39335
  taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
39336
+ const todoSync = new TodoSyncService(client, directory);
39337
+ await todoSync.start();
39338
+ taskToastManager.setTodoSync(todoSync);
39339
+ const cleanupScheduler = new CleanupScheduler(directory);
39340
+ cleanupScheduler.start();
39057
39341
  const handlerContext = {
39058
39342
  client,
39059
39343
  directory,
@@ -39110,7 +39394,20 @@ var OrchestratorPlugin = async (input) => {
39110
39394
  // -----------------------------------------------------------------
39111
39395
  // Event hook - handles OpenCode events
39112
39396
  // -----------------------------------------------------------------
39113
- event: createEventHandler(handlerContext),
39397
+ // -----------------------------------------------------------------
39398
+ // Event hook - handles OpenCode events
39399
+ // -----------------------------------------------------------------
39400
+ event: async (payload) => {
39401
+ const result = await createEventHandler(handlerContext)(payload);
39402
+ const { event } = payload;
39403
+ if (event.type === "session.created" && event.properties) {
39404
+ const sessionID = event.properties.sessionID || event.properties.id || event.properties.info?.sessionID;
39405
+ if (sessionID) {
39406
+ todoSync.registerSession(sessionID);
39407
+ }
39408
+ }
39409
+ return result;
39410
+ },
39114
39411
  // -----------------------------------------------------------------
39115
39412
  // chat.message hook - intercepts commands and sets up sessions
39116
39413
  // -----------------------------------------------------------------
@@ -47,6 +47,10 @@ export declare const TODO_CONSTANTS: {
47
47
  readonly PROGRESS: "progress";
48
48
  readonly FAILED: "failed";
49
49
  };
50
+ readonly PREFIX: {
51
+ readonly TASK: "task-";
52
+ readonly FILE: "file-task-";
53
+ };
50
54
  };
51
55
  export declare const TUI_CONSTANTS: {
52
56
  readonly BAR_WIDTH: 30;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "opencode-orchestrator",
3
3
  "displayName": "OpenCode Orchestrator",
4
4
  "description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
5
- "version": "1.2.11",
5
+ "version": "1.2.14",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {