@opencode_weave/weave 0.7.1 → 0.7.3

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.
@@ -84,6 +84,7 @@ export declare const ExperimentalConfigSchema: z.ZodObject<{
84
84
  plugin_load_timeout_ms: z.ZodOptional<z.ZodNumber>;
85
85
  context_window_warning_threshold: z.ZodOptional<z.ZodNumber>;
86
86
  context_window_critical_threshold: z.ZodOptional<z.ZodNumber>;
87
+ task_system: z.ZodDefault<z.ZodBoolean>;
87
88
  }, z.core.$strip>;
88
89
  export declare const DelegationTriggerSchema: z.ZodObject<{
89
90
  domain: z.ZodString;
@@ -258,6 +259,7 @@ export declare const WeaveConfigSchema: z.ZodObject<{
258
259
  plugin_load_timeout_ms: z.ZodOptional<z.ZodNumber>;
259
260
  context_window_warning_threshold: z.ZodOptional<z.ZodNumber>;
260
261
  context_window_critical_threshold: z.ZodOptional<z.ZodNumber>;
262
+ task_system: z.ZodDefault<z.ZodBoolean>;
261
263
  }, z.core.$strip>>;
262
264
  workflows: z.ZodOptional<z.ZodObject<{
263
265
  disabled_workflows: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -0,0 +1,6 @@
1
+ export { createTaskCreateTool, createTaskUpdateTool, createTaskListTool } from "./tools";
2
+ export { TaskStatus, TaskObjectSchema, TaskStatusSchema } from "./types";
3
+ export type { TaskObject, TaskCreateInput, TaskUpdateInput, TaskListInput } from "./types";
4
+ export { getTaskDir, generateTaskId, readTask, writeTask, readAllTasks } from "./storage";
5
+ export { syncTaskToTodo, syncTaskTodoUpdate, syncAllTasksToTodos } from "./todo-sync";
6
+ export type { TodoWriter, TodoInfo } from "./todo-sync";
@@ -0,0 +1,38 @@
1
+ import { type TaskObject } from "./types";
2
+ /**
3
+ * Derive the task storage directory for a given project.
4
+ * Uses the opencode config dir (~/.config/opencode by default) + sanitized project slug.
5
+ */
6
+ export declare function getTaskDir(directory: string, configDir?: string): string;
7
+ /** Generate a unique task ID */
8
+ export declare function generateTaskId(): string;
9
+ /**
10
+ * Read and parse a JSON file safely. Returns null for missing, corrupt, or invalid data.
11
+ */
12
+ export declare function readJsonSafe<T>(filePath: string, schema: {
13
+ parse: (data: unknown) => T;
14
+ }): T | null;
15
+ /**
16
+ * Write JSON atomically: write to a temp file then rename.
17
+ * This prevents partial writes from corrupting the target file.
18
+ */
19
+ export declare function writeJsonAtomic(filePath: string, data: unknown): void;
20
+ /**
21
+ * Acquire a file-based lock. Uses exclusive file creation (wx flag).
22
+ * Returns a release function on success, null on failure.
23
+ *
24
+ * Stale locks older than `staleThresholdMs` (default 30s) are automatically broken.
25
+ */
26
+ export declare function acquireLock(lockPath: string, staleThresholdMs?: number): (() => void) | null;
27
+ /** Ensure a directory exists */
28
+ export declare function ensureDir(dirPath: string): void;
29
+ /** List task files (T-*.json) in the task directory */
30
+ export declare function listTaskFiles(taskDir: string): string[];
31
+ /** Get the file path for a task by ID */
32
+ export declare function getTaskFilePath(taskDir: string, taskId: string): string;
33
+ /** Read a single task from file storage */
34
+ export declare function readTask(taskDir: string, taskId: string): TaskObject | null;
35
+ /** Write a single task to file storage (atomic) */
36
+ export declare function writeTask(taskDir: string, task: TaskObject): void;
37
+ /** Read all tasks from a task directory */
38
+ export declare function readAllTasks(taskDir: string): TaskObject[];
@@ -0,0 +1,38 @@
1
+ import type { TaskObject } from "./types";
2
+ /** TodoInfo matches the shape expected by OpenCode's todo sidebar */
3
+ export interface TodoInfo {
4
+ id?: string;
5
+ content: string;
6
+ status: "pending" | "in_progress" | "completed";
7
+ priority?: "high" | "medium" | "low";
8
+ }
9
+ /** TodoWriter interface — abstracts the OpenCode todo write API */
10
+ export interface TodoWriter {
11
+ read(sessionId: string): Promise<TodoInfo[]>;
12
+ update(sessionId: string, todos: TodoInfo[]): Promise<void>;
13
+ }
14
+ /**
15
+ * Map a TaskObject to a TodoInfo for the sidebar.
16
+ * Returns null for deleted tasks (they should be removed from the sidebar).
17
+ */
18
+ export declare function syncTaskToTodo(task: TaskObject): TodoInfo | null;
19
+ /**
20
+ * Check if two todo items match by ID first, then by content as fallback.
21
+ */
22
+ export declare function todosMatch(a: TodoInfo, b: TodoInfo): boolean;
23
+ /**
24
+ * Sync a single task to the todo sidebar.
25
+ * This is the anti-obliteration mechanism:
26
+ * 1. Read current todos
27
+ * 2. Filter out the matching item (by ID or content)
28
+ * 3. Push the updated item (or omit it if deleted)
29
+ * 4. Write back the full list
30
+ *
31
+ * Non-task todos (those not matching any task ID) survive intact.
32
+ */
33
+ export declare function syncTaskTodoUpdate(writer: TodoWriter | null, sessionId: string, task: TaskObject): Promise<void>;
34
+ /**
35
+ * Sync all tasks to the todo sidebar, preserving non-task todos.
36
+ * Used for bulk reconciliation.
37
+ */
38
+ export declare function syncAllTasksToTodos(writer: TodoWriter | null, sessionId: string, tasks: TaskObject[]): Promise<void>;
@@ -0,0 +1,3 @@
1
+ export { createTaskCreateTool } from "./task-create";
2
+ export { createTaskUpdateTool } from "./task-update";
3
+ export { createTaskListTool } from "./task-list";
@@ -0,0 +1,9 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import { type TodoWriter } from "../todo-sync";
3
+ declare const TASK_ID_PATTERN: RegExp;
4
+ export declare function createTaskCreateTool(options: {
5
+ directory: string;
6
+ configDir?: string;
7
+ todoWriter?: TodoWriter | null;
8
+ }): ToolDefinition;
9
+ export { TASK_ID_PATTERN };
@@ -0,0 +1,5 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ export declare function createTaskListTool(options: {
3
+ directory: string;
4
+ configDir?: string;
5
+ }): ToolDefinition;
@@ -0,0 +1,7 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import { type TodoWriter } from "../todo-sync";
3
+ export declare function createTaskUpdateTool(options: {
4
+ directory: string;
5
+ configDir?: string;
6
+ todoWriter?: TodoWriter | null;
7
+ }): ToolDefinition;
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ /** Task status values */
3
+ export declare const TaskStatus: {
4
+ readonly PENDING: "pending";
5
+ readonly IN_PROGRESS: "in_progress";
6
+ readonly COMPLETED: "completed";
7
+ readonly DELETED: "deleted";
8
+ };
9
+ export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];
10
+ export declare const TaskStatusSchema: z.ZodEnum<{
11
+ pending: "pending";
12
+ completed: "completed";
13
+ in_progress: "in_progress";
14
+ deleted: "deleted";
15
+ }>;
16
+ /**
17
+ * Core task object — simplified from OmO's schema.
18
+ * Drops: activeForm, owner, repoURL, parentID (per design decision D4).
19
+ */
20
+ export declare const TaskObjectSchema: z.ZodObject<{
21
+ id: z.ZodString;
22
+ subject: z.ZodString;
23
+ description: z.ZodString;
24
+ status: z.ZodEnum<{
25
+ pending: "pending";
26
+ completed: "completed";
27
+ in_progress: "in_progress";
28
+ deleted: "deleted";
29
+ }>;
30
+ threadID: z.ZodString;
31
+ blocks: z.ZodDefault<z.ZodArray<z.ZodString>>;
32
+ blockedBy: z.ZodDefault<z.ZodArray<z.ZodString>>;
33
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
34
+ }, z.core.$strip>;
35
+ export type TaskObject = z.infer<typeof TaskObjectSchema>;
36
+ /** Input schema for task_create tool */
37
+ export declare const TaskCreateInputSchema: z.ZodObject<{
38
+ subject: z.ZodString;
39
+ description: z.ZodOptional<z.ZodString>;
40
+ blocks: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
+ blockedBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
43
+ }, z.core.$strip>;
44
+ export type TaskCreateInput = z.infer<typeof TaskCreateInputSchema>;
45
+ /** Input schema for task_update tool */
46
+ export declare const TaskUpdateInputSchema: z.ZodObject<{
47
+ id: z.ZodString;
48
+ subject: z.ZodOptional<z.ZodString>;
49
+ description: z.ZodOptional<z.ZodString>;
50
+ status: z.ZodOptional<z.ZodEnum<{
51
+ pending: "pending";
52
+ completed: "completed";
53
+ in_progress: "in_progress";
54
+ deleted: "deleted";
55
+ }>>;
56
+ addBlocks: z.ZodOptional<z.ZodArray<z.ZodString>>;
57
+ addBlockedBy: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
59
+ }, z.core.$strip>;
60
+ export type TaskUpdateInput = z.infer<typeof TaskUpdateInputSchema>;
61
+ /** Input schema for task_list tool (no args needed for PoC) */
62
+ export declare const TaskListInputSchema: z.ZodObject<{}, z.core.$strip>;
63
+ export type TaskListInput = z.infer<typeof TaskListInputSchema>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { join as join13 } from "path";
2
+ import { join as join14 } from "path";
3
3
 
4
4
  // src/config/loader.ts
5
5
  import { existsSync as existsSync2, readFileSync } from "node:fs";
@@ -53,7 +53,8 @@ var TmuxConfigSchema = z.object({
53
53
  var ExperimentalConfigSchema = z.object({
54
54
  plugin_load_timeout_ms: z.number().min(1000).optional(),
55
55
  context_window_warning_threshold: z.number().min(0).max(1).optional(),
56
- context_window_critical_threshold: z.number().min(0).max(1).optional()
56
+ context_window_critical_threshold: z.number().min(0).max(1).optional(),
57
+ task_system: z.boolean().default(true)
57
58
  });
58
59
  var DelegationTriggerSchema = z.object({
59
60
  domain: z.string(),
@@ -1247,6 +1248,8 @@ Use this structure:
1247
1248
  \`\`\`
1248
1249
 
1249
1250
  CRITICAL: Use \`- [ ]\` checkboxes for ALL actionable items. The /start-work system tracks progress by counting these checkboxes.
1251
+
1252
+ Use the exact section headings shown in the template above (\`## TL;DR\`, \`## Context\`, \`## Objectives\`, \`## TODOs\`, \`## Verification\`). Consistent headings help downstream tooling parse the plan.
1250
1253
  </PlanOutput>
1251
1254
 
1252
1255
  <Constraints>
@@ -1909,7 +1912,10 @@ var KNOWN_TOOL_NAMES = new Set([
1909
1912
  "call_weave_agent",
1910
1913
  "webfetch",
1911
1914
  "todowrite",
1912
- "skill"
1915
+ "skill",
1916
+ "task_create",
1917
+ "task_update",
1918
+ "task_list"
1913
1919
  ]);
1914
1920
  var AGENT_NAME_PATTERN = /^[a-z][a-z0-9_-]*$/;
1915
1921
  function parseFallbackModels(models) {
@@ -2289,6 +2295,275 @@ function createSkillResolver(discovered) {
2289
2295
  return resolveMultipleSkills(skillNames, disabledSkills, discovered);
2290
2296
  };
2291
2297
  }
2298
+ // src/features/task-system/tools/task-create.ts
2299
+ import { tool } from "@opencode-ai/plugin";
2300
+
2301
+ // src/features/task-system/storage.ts
2302
+ import { mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync4, renameSync, unlinkSync, readdirSync as readdirSync2, statSync, openSync, closeSync } from "fs";
2303
+ import { join as join5, basename } from "path";
2304
+ import { randomUUID } from "crypto";
2305
+
2306
+ // src/features/task-system/types.ts
2307
+ import { z as z2 } from "zod";
2308
+ var TaskStatus = {
2309
+ PENDING: "pending",
2310
+ IN_PROGRESS: "in_progress",
2311
+ COMPLETED: "completed",
2312
+ DELETED: "deleted"
2313
+ };
2314
+ var TaskStatusSchema = z2.enum(["pending", "in_progress", "completed", "deleted"]);
2315
+ var TaskObjectSchema = z2.object({
2316
+ id: z2.string(),
2317
+ subject: z2.string(),
2318
+ description: z2.string(),
2319
+ status: TaskStatusSchema,
2320
+ threadID: z2.string(),
2321
+ blocks: z2.array(z2.string()).default([]),
2322
+ blockedBy: z2.array(z2.string()).default([]),
2323
+ metadata: z2.record(z2.string(), z2.unknown()).optional()
2324
+ });
2325
+ var TaskCreateInputSchema = z2.object({
2326
+ subject: z2.string().describe("Short title for the task (required)"),
2327
+ description: z2.string().optional().describe("Detailed description of the task"),
2328
+ blocks: z2.array(z2.string()).optional().describe("Task IDs that this task blocks"),
2329
+ blockedBy: z2.array(z2.string()).optional().describe("Task IDs that block this task"),
2330
+ metadata: z2.record(z2.string(), z2.unknown()).optional().describe("Arbitrary key-value metadata")
2331
+ });
2332
+ var TaskUpdateInputSchema = z2.object({
2333
+ id: z2.string().describe("Task ID to update (required, format: T-{uuid})"),
2334
+ subject: z2.string().optional().describe("New subject/title"),
2335
+ description: z2.string().optional().describe("New description"),
2336
+ status: TaskStatusSchema.optional().describe("New status"),
2337
+ addBlocks: z2.array(z2.string()).optional().describe("Task IDs to add to blocks (additive, no replacement)"),
2338
+ addBlockedBy: z2.array(z2.string()).optional().describe("Task IDs to add to blockedBy (additive, no replacement)"),
2339
+ metadata: z2.record(z2.string(), z2.unknown()).optional().describe("Metadata to merge (null values delete keys)")
2340
+ });
2341
+ var TaskListInputSchema = z2.object({});
2342
+
2343
+ // src/features/task-system/storage.ts
2344
+ function getTaskDir(directory, configDir) {
2345
+ const base = configDir ?? join5(getHomeDir(), ".config", "opencode");
2346
+ const slug = sanitizeSlug(basename(directory));
2347
+ return join5(base, "tasks", slug);
2348
+ }
2349
+ function getHomeDir() {
2350
+ return process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
2351
+ }
2352
+ function sanitizeSlug(name) {
2353
+ return name.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "default";
2354
+ }
2355
+ function generateTaskId() {
2356
+ return `T-${randomUUID()}`;
2357
+ }
2358
+ function readJsonSafe(filePath, schema) {
2359
+ try {
2360
+ const raw = readFileSync4(filePath, "utf-8");
2361
+ const parsed = JSON.parse(raw);
2362
+ return schema.parse(parsed);
2363
+ } catch {
2364
+ return null;
2365
+ }
2366
+ }
2367
+ function writeJsonAtomic(filePath, data) {
2368
+ const dir = join5(filePath, "..");
2369
+ mkdirSync2(dir, { recursive: true });
2370
+ const tmpPath = `${filePath}.tmp`;
2371
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
2372
+ renameSync(tmpPath, filePath);
2373
+ }
2374
+ function ensureDir(dirPath) {
2375
+ mkdirSync2(dirPath, { recursive: true });
2376
+ }
2377
+ function listTaskFiles(taskDir) {
2378
+ try {
2379
+ return readdirSync2(taskDir).filter((f) => f.startsWith("T-") && f.endsWith(".json")).map((f) => join5(taskDir, f));
2380
+ } catch {
2381
+ return [];
2382
+ }
2383
+ }
2384
+ function getTaskFilePath(taskDir, taskId) {
2385
+ return join5(taskDir, `${taskId}.json`);
2386
+ }
2387
+ function readTask(taskDir, taskId) {
2388
+ return readJsonSafe(getTaskFilePath(taskDir, taskId), TaskObjectSchema);
2389
+ }
2390
+ function writeTask(taskDir, task) {
2391
+ writeJsonAtomic(getTaskFilePath(taskDir, task.id), task);
2392
+ }
2393
+ function readAllTasks(taskDir) {
2394
+ const files = listTaskFiles(taskDir);
2395
+ const tasks = [];
2396
+ for (const file of files) {
2397
+ const task = readJsonSafe(file, TaskObjectSchema);
2398
+ if (task)
2399
+ tasks.push(task);
2400
+ }
2401
+ return tasks;
2402
+ }
2403
+
2404
+ // src/features/task-system/todo-sync.ts
2405
+ function syncTaskToTodo(task) {
2406
+ if (task.status === TaskStatus.DELETED) {
2407
+ return null;
2408
+ }
2409
+ const statusMap = {
2410
+ [TaskStatus.PENDING]: "pending",
2411
+ [TaskStatus.IN_PROGRESS]: "in_progress",
2412
+ [TaskStatus.COMPLETED]: "completed"
2413
+ };
2414
+ const priority = task.metadata?.priority ?? undefined;
2415
+ return {
2416
+ id: task.id,
2417
+ content: task.subject,
2418
+ status: statusMap[task.status] ?? "pending",
2419
+ ...priority ? { priority } : {}
2420
+ };
2421
+ }
2422
+ function todosMatch(a, b) {
2423
+ if (a.id && b.id)
2424
+ return a.id === b.id;
2425
+ return a.content === b.content;
2426
+ }
2427
+ async function syncTaskTodoUpdate(writer, sessionId, task) {
2428
+ if (!writer) {
2429
+ log("[task-sync] No todo writer available — skipping sidebar sync");
2430
+ return;
2431
+ }
2432
+ try {
2433
+ const currentTodos = await writer.read(sessionId);
2434
+ const todoItem = syncTaskToTodo(task);
2435
+ const filtered = currentTodos.filter((t) => !todosMatch(t, { id: task.id, content: task.subject, status: "pending" }));
2436
+ if (todoItem) {
2437
+ filtered.push(todoItem);
2438
+ }
2439
+ await writer.update(sessionId, filtered);
2440
+ } catch (err) {
2441
+ log("[task-sync] Failed to sync task to sidebar (non-fatal)", { taskId: task.id, error: String(err) });
2442
+ }
2443
+ }
2444
+
2445
+ // src/features/task-system/tools/task-create.ts
2446
+ function createTaskCreateTool(options) {
2447
+ const { directory, configDir, todoWriter = null } = options;
2448
+ return tool({
2449
+ description: "Create a new task. Use this instead of todowrite for task tracking. " + "Each task gets a unique ID and is stored atomically — creating a task never destroys existing tasks or todos.",
2450
+ args: {
2451
+ subject: tool.schema.string().describe("Short title for the task (required)"),
2452
+ description: tool.schema.string().optional().describe("Detailed description of the task"),
2453
+ blocks: tool.schema.array(tool.schema.string()).optional().describe("Task IDs that this task blocks"),
2454
+ blockedBy: tool.schema.array(tool.schema.string()).optional().describe("Task IDs that block this task"),
2455
+ metadata: tool.schema.record(tool.schema.string(), tool.schema.unknown()).optional().describe("Arbitrary key-value metadata")
2456
+ },
2457
+ async execute(args, context) {
2458
+ const taskDir = getTaskDir(directory, configDir);
2459
+ ensureDir(taskDir);
2460
+ const task = {
2461
+ id: generateTaskId(),
2462
+ subject: args.subject,
2463
+ description: args.description ?? "",
2464
+ status: TaskStatus.PENDING,
2465
+ threadID: context.sessionID,
2466
+ blocks: args.blocks ?? [],
2467
+ blockedBy: args.blockedBy ?? [],
2468
+ metadata: args.metadata
2469
+ };
2470
+ writeTask(taskDir, task);
2471
+ log("[task-create] Created task", { id: task.id, subject: task.subject });
2472
+ await syncTaskTodoUpdate(todoWriter, context.sessionID, task);
2473
+ return JSON.stringify({ task: { id: task.id, subject: task.subject } });
2474
+ }
2475
+ });
2476
+ }
2477
+ // src/features/task-system/tools/task-update.ts
2478
+ import { tool as tool2 } from "@opencode-ai/plugin";
2479
+ var TASK_ID_PATTERN = /^T-[A-Za-z0-9-]+$/;
2480
+ function createTaskUpdateTool(options) {
2481
+ const { directory, configDir, todoWriter = null } = options;
2482
+ return tool2({
2483
+ description: "Update an existing task by ID. Modifies only the specified fields — " + "other tasks and non-task todos are completely untouched. " + "blocks/blockedBy are additive (appended, never replaced).",
2484
+ args: {
2485
+ id: tool2.schema.string().describe("Task ID to update (required, format: T-{uuid})"),
2486
+ subject: tool2.schema.string().optional().describe("New subject/title"),
2487
+ description: tool2.schema.string().optional().describe("New description"),
2488
+ status: tool2.schema.enum(["pending", "in_progress", "completed", "deleted"]).optional().describe("New status"),
2489
+ addBlocks: tool2.schema.array(tool2.schema.string()).optional().describe("Task IDs to add to blocks (additive)"),
2490
+ addBlockedBy: tool2.schema.array(tool2.schema.string()).optional().describe("Task IDs to add to blockedBy (additive)"),
2491
+ metadata: tool2.schema.record(tool2.schema.string(), tool2.schema.unknown()).optional().describe("Metadata to merge (null values delete keys)")
2492
+ },
2493
+ async execute(args, context) {
2494
+ if (!TASK_ID_PATTERN.test(args.id)) {
2495
+ return JSON.stringify({ error: "invalid_task_id", message: `Invalid task ID format: ${args.id}. Expected T-{uuid}` });
2496
+ }
2497
+ const taskDir = getTaskDir(directory, configDir);
2498
+ const task = readTask(taskDir, args.id);
2499
+ if (!task) {
2500
+ return JSON.stringify({ error: "task_not_found", message: `Task ${args.id} not found` });
2501
+ }
2502
+ if (args.subject !== undefined)
2503
+ task.subject = args.subject;
2504
+ if (args.description !== undefined)
2505
+ task.description = args.description;
2506
+ if (args.status !== undefined)
2507
+ task.status = args.status;
2508
+ if (args.addBlocks?.length) {
2509
+ const existing = new Set(task.blocks);
2510
+ for (const b of args.addBlocks) {
2511
+ if (!existing.has(b)) {
2512
+ task.blocks.push(b);
2513
+ existing.add(b);
2514
+ }
2515
+ }
2516
+ }
2517
+ if (args.addBlockedBy?.length) {
2518
+ const existing = new Set(task.blockedBy);
2519
+ for (const b of args.addBlockedBy) {
2520
+ if (!existing.has(b)) {
2521
+ task.blockedBy.push(b);
2522
+ existing.add(b);
2523
+ }
2524
+ }
2525
+ }
2526
+ if (args.metadata) {
2527
+ const meta = task.metadata ?? {};
2528
+ for (const [key, value] of Object.entries(args.metadata)) {
2529
+ if (value === null) {
2530
+ delete meta[key];
2531
+ } else {
2532
+ meta[key] = value;
2533
+ }
2534
+ }
2535
+ task.metadata = Object.keys(meta).length > 0 ? meta : undefined;
2536
+ }
2537
+ writeTask(taskDir, task);
2538
+ log("[task-update] Updated task", { id: task.id });
2539
+ await syncTaskTodoUpdate(todoWriter, context.sessionID, task);
2540
+ return JSON.stringify({ task });
2541
+ }
2542
+ });
2543
+ }
2544
+ // src/features/task-system/tools/task-list.ts
2545
+ import { tool as tool3 } from "@opencode-ai/plugin";
2546
+ function createTaskListTool(options) {
2547
+ const { directory, configDir } = options;
2548
+ return tool3({
2549
+ description: "List all active tasks (pending and in_progress). " + "Excludes completed and deleted tasks. " + "Shows unresolved blockers for each task.",
2550
+ args: {},
2551
+ async execute(_args, _context) {
2552
+ const taskDir = getTaskDir(directory, configDir);
2553
+ const allTasks = readAllTasks(taskDir);
2554
+ const activeTasks = allTasks.filter((t) => t.status !== TaskStatus.COMPLETED && t.status !== TaskStatus.DELETED);
2555
+ const completedIds = new Set(allTasks.filter((t) => t.status === TaskStatus.COMPLETED).map((t) => t.id));
2556
+ const tasks = activeTasks.map((t) => ({
2557
+ id: t.id,
2558
+ subject: t.subject,
2559
+ status: t.status,
2560
+ blockedBy: t.blockedBy.filter((b) => !completedIds.has(b))
2561
+ }));
2562
+ log("[task-list] Listed tasks", { count: tasks.length });
2563
+ return JSON.stringify({ tasks });
2564
+ }
2565
+ });
2566
+ }
2292
2567
  // src/create-tools.ts
2293
2568
  async function createTools(options) {
2294
2569
  const { ctx, pluginConfig } = options;
@@ -2299,6 +2574,13 @@ async function createTools(options) {
2299
2574
  });
2300
2575
  const resolveSkillsFn = createSkillResolver(skillResult);
2301
2576
  const tools = {};
2577
+ if (pluginConfig.experimental?.task_system !== false) {
2578
+ const toolOptions = { directory: ctx.directory };
2579
+ tools.task_create = createTaskCreateTool(toolOptions);
2580
+ tools.task_update = createTaskUpdateTool(toolOptions);
2581
+ tools.task_list = createTaskListTool(toolOptions);
2582
+ log("[task-system] Registered task tools (task_create, task_update, task_list)");
2583
+ }
2302
2584
  return {
2303
2585
  tools,
2304
2586
  availableSkills: skillResult.skills,
@@ -2491,17 +2773,17 @@ var WORK_STATE_FILE = "state.json";
2491
2773
  var WORK_STATE_PATH = `${WEAVE_DIR}/${WORK_STATE_FILE}`;
2492
2774
  var PLANS_DIR = `${WEAVE_DIR}/plans`;
2493
2775
  // src/features/work-state/storage.ts
2494
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync, unlinkSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, statSync } from "fs";
2495
- import { join as join6, basename } from "path";
2776
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
2777
+ import { join as join7, basename as basename2 } from "path";
2496
2778
  import { execSync } from "child_process";
2497
2779
  var UNCHECKED_RE = /^[-*]\s*\[\s*\]/gm;
2498
2780
  var CHECKED_RE = /^[-*]\s*\[[xX]\]/gm;
2499
2781
  function readWorkState(directory) {
2500
- const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
2782
+ const filePath = join7(directory, WEAVE_DIR, WORK_STATE_FILE);
2501
2783
  try {
2502
2784
  if (!existsSync7(filePath))
2503
2785
  return null;
2504
- const raw = readFileSync5(filePath, "utf-8");
2786
+ const raw = readFileSync6(filePath, "utf-8");
2505
2787
  const parsed = JSON.parse(raw);
2506
2788
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
2507
2789
  return null;
@@ -2517,21 +2799,21 @@ function readWorkState(directory) {
2517
2799
  }
2518
2800
  function writeWorkState(directory, state) {
2519
2801
  try {
2520
- const dir = join6(directory, WEAVE_DIR);
2802
+ const dir = join7(directory, WEAVE_DIR);
2521
2803
  if (!existsSync7(dir)) {
2522
- mkdirSync2(dir, { recursive: true });
2804
+ mkdirSync3(dir, { recursive: true });
2523
2805
  }
2524
- writeFileSync(join6(dir, WORK_STATE_FILE), JSON.stringify(state, null, 2), "utf-8");
2806
+ writeFileSync2(join7(dir, WORK_STATE_FILE), JSON.stringify(state, null, 2), "utf-8");
2525
2807
  return true;
2526
2808
  } catch {
2527
2809
  return false;
2528
2810
  }
2529
2811
  }
2530
2812
  function clearWorkState(directory) {
2531
- const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
2813
+ const filePath = join7(directory, WEAVE_DIR, WORK_STATE_FILE);
2532
2814
  try {
2533
2815
  if (existsSync7(filePath)) {
2534
- unlinkSync(filePath);
2816
+ unlinkSync2(filePath);
2535
2817
  }
2536
2818
  return true;
2537
2819
  } catch {
@@ -2572,13 +2854,13 @@ function getHeadSha(directory) {
2572
2854
  }
2573
2855
  }
2574
2856
  function findPlans(directory) {
2575
- const plansDir = join6(directory, PLANS_DIR);
2857
+ const plansDir = join7(directory, PLANS_DIR);
2576
2858
  try {
2577
2859
  if (!existsSync7(plansDir))
2578
2860
  return [];
2579
- const files = readdirSync2(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
2580
- const fullPath = join6(plansDir, f);
2581
- const stat = statSync(fullPath);
2861
+ const files = readdirSync3(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
2862
+ const fullPath = join7(plansDir, f);
2863
+ const stat = statSync2(fullPath);
2582
2864
  return { path: fullPath, mtime: stat.mtimeMs };
2583
2865
  }).sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
2584
2866
  return files;
@@ -2591,7 +2873,7 @@ function getPlanProgress(planPath) {
2591
2873
  return { total: 0, completed: 0, isComplete: true };
2592
2874
  }
2593
2875
  try {
2594
- const content = readFileSync5(planPath, "utf-8");
2876
+ const content = readFileSync6(planPath, "utf-8");
2595
2877
  const unchecked = content.match(UNCHECKED_RE) || [];
2596
2878
  const checked = content.match(CHECKED_RE) || [];
2597
2879
  const total = unchecked.length + checked.length;
@@ -2606,7 +2888,7 @@ function getPlanProgress(planPath) {
2606
2888
  }
2607
2889
  }
2608
2890
  function getPlanName(planPath) {
2609
- return basename(planPath, ".md");
2891
+ return basename2(planPath, ".md");
2610
2892
  }
2611
2893
  function pauseWork(directory) {
2612
2894
  const state = readWorkState(directory);
@@ -2623,7 +2905,7 @@ function resumeWork(directory) {
2623
2905
  return writeWorkState(directory, state);
2624
2906
  }
2625
2907
  // src/features/work-state/validation.ts
2626
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
2908
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
2627
2909
  import { resolve as resolve3, sep as sep2 } from "path";
2628
2910
  function validatePlan(planPath, projectDir) {
2629
2911
  const errors = [];
@@ -2646,13 +2928,13 @@ function validatePlan(planPath, projectDir) {
2646
2928
  });
2647
2929
  return { valid: false, errors, warnings };
2648
2930
  }
2649
- const content = readFileSync6(resolvedPlanPath, "utf-8");
2931
+ const content = readFileSync7(resolvedPlanPath, "utf-8");
2650
2932
  validateStructure(content, errors, warnings);
2651
2933
  validateCheckboxes(content, errors, warnings);
2652
2934
  validateFileReferences(content, projectDir, warnings);
2653
2935
  validateNumbering(content, errors, warnings);
2654
2936
  validateEffortEstimate(content, warnings);
2655
- validateVerificationSection(content, errors);
2937
+ validateVerificationSection(content, warnings);
2656
2938
  return {
2657
2939
  valid: errors.length === 0,
2658
2940
  errors,
@@ -2684,15 +2966,15 @@ function hasSection(content, heading) {
2684
2966
  return content.split(`
2685
2967
  `).some((line) => line.trim() === heading);
2686
2968
  }
2687
- function validateStructure(content, errors, warnings) {
2688
- const requiredSections = [
2689
- ["## TL;DR", "Missing required section: ## TL;DR"],
2690
- ["## TODOs", "Missing required section: ## TODOs"],
2691
- ["## Verification", "Missing required section: ## Verification"]
2969
+ function validateStructure(content, _errors, warnings) {
2970
+ const expectedSections = [
2971
+ ["## TL;DR", "Missing expected section: ## TL;DR"],
2972
+ ["## TODOs", "Missing expected section: ## TODOs"],
2973
+ ["## Verification", "Missing expected section: ## Verification"]
2692
2974
  ];
2693
- for (const [heading, message] of requiredSections) {
2975
+ for (const [heading, message] of expectedSections) {
2694
2976
  if (!hasSection(content, heading)) {
2695
- errors.push({ severity: "error", category: "structure", message });
2977
+ warnings.push({ severity: "warning", category: "structure", message });
2696
2978
  }
2697
2979
  }
2698
2980
  const optionalSections = [
@@ -2708,6 +2990,14 @@ function validateStructure(content, errors, warnings) {
2708
2990
  function validateCheckboxes(content, errors, warnings) {
2709
2991
  const todosSection = extractSection(content, "## TODOs");
2710
2992
  if (todosSection === null) {
2993
+ const hasAnyCheckbox = /^- \[[ x]\] /m.test(content);
2994
+ if (!hasAnyCheckbox) {
2995
+ errors.push({
2996
+ severity: "error",
2997
+ category: "checkboxes",
2998
+ message: "Plan contains no checkboxes (- [ ] or - [x]) — nothing to execute"
2999
+ });
3000
+ }
2711
3001
  return;
2712
3002
  }
2713
3003
  const checkboxPattern = /^- \[[ x]\] /m;
@@ -2888,17 +3178,17 @@ function validateEffortEstimate(content, warnings) {
2888
3178
  });
2889
3179
  }
2890
3180
  }
2891
- function validateVerificationSection(content, errors) {
3181
+ function validateVerificationSection(content, warnings) {
2892
3182
  const verificationSection = extractSection(content, "## Verification");
2893
3183
  if (verificationSection === null) {
2894
3184
  return;
2895
3185
  }
2896
3186
  const hasCheckbox = /^- \[[ x]\] /m.test(verificationSection);
2897
3187
  if (!hasCheckbox) {
2898
- errors.push({
2899
- severity: "error",
3188
+ warnings.push({
3189
+ severity: "warning",
2900
3190
  category: "verification",
2901
- message: "## Verification section contains no checkboxes — at least one verifiable condition is required"
3191
+ message: "## Verification section contains no checkboxes — consider adding verifiable conditions"
2902
3192
  });
2903
3193
  }
2904
3194
  }
@@ -2909,8 +3199,8 @@ var ACTIVE_INSTANCE_FILE = "active-instance.json";
2909
3199
  var WORKFLOWS_DIR_PROJECT = ".opencode/workflows";
2910
3200
  var WORKFLOWS_DIR_USER = "workflows";
2911
3201
  // src/features/workflow/storage.ts
2912
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
2913
- import { join as join7 } from "path";
3202
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync4 } from "fs";
3203
+ import { join as join8 } from "path";
2914
3204
  import { randomBytes } from "node:crypto";
2915
3205
  function generateInstanceId() {
2916
3206
  return `wf_${randomBytes(4).toString("hex")}`;
@@ -2946,11 +3236,11 @@ function createWorkflowInstance(definition, definitionPath, goal, sessionId) {
2946
3236
  };
2947
3237
  }
2948
3238
  function readWorkflowInstance(directory, instanceId) {
2949
- const filePath = join7(directory, WORKFLOWS_STATE_DIR, instanceId, INSTANCE_STATE_FILE);
3239
+ const filePath = join8(directory, WORKFLOWS_STATE_DIR, instanceId, INSTANCE_STATE_FILE);
2950
3240
  try {
2951
3241
  if (!existsSync9(filePath))
2952
3242
  return null;
2953
- const raw = readFileSync7(filePath, "utf-8");
3243
+ const raw = readFileSync8(filePath, "utf-8");
2954
3244
  const parsed = JSON.parse(raw);
2955
3245
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
2956
3246
  return null;
@@ -2963,22 +3253,22 @@ function readWorkflowInstance(directory, instanceId) {
2963
3253
  }
2964
3254
  function writeWorkflowInstance(directory, instance) {
2965
3255
  try {
2966
- const dir = join7(directory, WORKFLOWS_STATE_DIR, instance.instance_id);
3256
+ const dir = join8(directory, WORKFLOWS_STATE_DIR, instance.instance_id);
2967
3257
  if (!existsSync9(dir)) {
2968
- mkdirSync3(dir, { recursive: true });
3258
+ mkdirSync4(dir, { recursive: true });
2969
3259
  }
2970
- writeFileSync2(join7(dir, INSTANCE_STATE_FILE), JSON.stringify(instance, null, 2), "utf-8");
3260
+ writeFileSync3(join8(dir, INSTANCE_STATE_FILE), JSON.stringify(instance, null, 2), "utf-8");
2971
3261
  return true;
2972
3262
  } catch {
2973
3263
  return false;
2974
3264
  }
2975
3265
  }
2976
3266
  function readActiveInstance(directory) {
2977
- const filePath = join7(directory, WORKFLOWS_STATE_DIR, ACTIVE_INSTANCE_FILE);
3267
+ const filePath = join8(directory, WORKFLOWS_STATE_DIR, ACTIVE_INSTANCE_FILE);
2978
3268
  try {
2979
3269
  if (!existsSync9(filePath))
2980
3270
  return null;
2981
- const raw = readFileSync7(filePath, "utf-8");
3271
+ const raw = readFileSync8(filePath, "utf-8");
2982
3272
  const parsed = JSON.parse(raw);
2983
3273
  if (!parsed || typeof parsed !== "object" || typeof parsed.instance_id !== "string")
2984
3274
  return null;
@@ -2989,22 +3279,22 @@ function readActiveInstance(directory) {
2989
3279
  }
2990
3280
  function setActiveInstance(directory, instanceId) {
2991
3281
  try {
2992
- const dir = join7(directory, WORKFLOWS_STATE_DIR);
3282
+ const dir = join8(directory, WORKFLOWS_STATE_DIR);
2993
3283
  if (!existsSync9(dir)) {
2994
- mkdirSync3(dir, { recursive: true });
3284
+ mkdirSync4(dir, { recursive: true });
2995
3285
  }
2996
3286
  const pointer = { instance_id: instanceId };
2997
- writeFileSync2(join7(dir, ACTIVE_INSTANCE_FILE), JSON.stringify(pointer, null, 2), "utf-8");
3287
+ writeFileSync3(join8(dir, ACTIVE_INSTANCE_FILE), JSON.stringify(pointer, null, 2), "utf-8");
2998
3288
  return true;
2999
3289
  } catch {
3000
3290
  return false;
3001
3291
  }
3002
3292
  }
3003
3293
  function clearActiveInstance(directory) {
3004
- const filePath = join7(directory, WORKFLOWS_STATE_DIR, ACTIVE_INSTANCE_FILE);
3294
+ const filePath = join8(directory, WORKFLOWS_STATE_DIR, ACTIVE_INSTANCE_FILE);
3005
3295
  try {
3006
3296
  if (existsSync9(filePath)) {
3007
- unlinkSync2(filePath);
3297
+ unlinkSync3(filePath);
3008
3298
  }
3009
3299
  return true;
3010
3300
  } catch {
@@ -3024,35 +3314,35 @@ import * as os3 from "os";
3024
3314
  import { parse as parseJsonc } from "jsonc-parser";
3025
3315
 
3026
3316
  // src/features/workflow/schema.ts
3027
- import { z as z2 } from "zod";
3028
- var CompletionConfigSchema = z2.object({
3029
- method: z2.enum(["user_confirm", "plan_created", "plan_complete", "review_verdict", "agent_signal"]),
3030
- plan_name: z2.string().optional(),
3031
- keywords: z2.array(z2.string()).optional()
3317
+ import { z as z3 } from "zod";
3318
+ var CompletionConfigSchema = z3.object({
3319
+ method: z3.enum(["user_confirm", "plan_created", "plan_complete", "review_verdict", "agent_signal"]),
3320
+ plan_name: z3.string().optional(),
3321
+ keywords: z3.array(z3.string()).optional()
3032
3322
  });
3033
- var ArtifactRefSchema = z2.object({
3034
- name: z2.string(),
3035
- description: z2.string().optional()
3323
+ var ArtifactRefSchema = z3.object({
3324
+ name: z3.string(),
3325
+ description: z3.string().optional()
3036
3326
  });
3037
- var StepArtifactsSchema = z2.object({
3038
- inputs: z2.array(ArtifactRefSchema).optional(),
3039
- outputs: z2.array(ArtifactRefSchema).optional()
3327
+ var StepArtifactsSchema = z3.object({
3328
+ inputs: z3.array(ArtifactRefSchema).optional(),
3329
+ outputs: z3.array(ArtifactRefSchema).optional()
3040
3330
  });
3041
- var WorkflowStepSchema = z2.object({
3042
- id: z2.string().regex(/^[a-z][a-z0-9-]*$/, "Step ID must be lowercase alphanumeric with hyphens"),
3043
- name: z2.string(),
3044
- type: z2.enum(["interactive", "autonomous", "gate"]),
3045
- agent: z2.string(),
3046
- prompt: z2.string(),
3331
+ var WorkflowStepSchema = z3.object({
3332
+ id: z3.string().regex(/^[a-z][a-z0-9-]*$/, "Step ID must be lowercase alphanumeric with hyphens"),
3333
+ name: z3.string(),
3334
+ type: z3.enum(["interactive", "autonomous", "gate"]),
3335
+ agent: z3.string(),
3336
+ prompt: z3.string(),
3047
3337
  completion: CompletionConfigSchema,
3048
3338
  artifacts: StepArtifactsSchema.optional(),
3049
- on_reject: z2.enum(["pause", "fail"]).optional()
3339
+ on_reject: z3.enum(["pause", "fail"]).optional()
3050
3340
  });
3051
- var WorkflowDefinitionSchema = z2.object({
3052
- name: z2.string().regex(/^[a-z][a-z0-9-]*$/, "Workflow name must be lowercase alphanumeric with hyphens"),
3053
- description: z2.string().optional(),
3054
- version: z2.number().int().positive(),
3055
- steps: z2.array(WorkflowStepSchema).min(1, "Workflow must have at least one step")
3341
+ var WorkflowDefinitionSchema = z3.object({
3342
+ name: z3.string().regex(/^[a-z][a-z0-9-]*$/, "Workflow name must be lowercase alphanumeric with hyphens"),
3343
+ description: z3.string().optional(),
3344
+ version: z3.number().int().positive(),
3345
+ steps: z3.array(WorkflowStepSchema).min(1, "Workflow must have at least one step")
3056
3346
  });
3057
3347
 
3058
3348
  // src/features/workflow/discovery.ts
@@ -3194,7 +3484,7 @@ function truncateSummary(text) {
3194
3484
  }
3195
3485
  // src/features/workflow/completion.ts
3196
3486
  import { existsSync as existsSync11 } from "fs";
3197
- import { join as join9 } from "path";
3487
+ import { join as join10 } from "path";
3198
3488
  var DEFAULT_CONFIRM_KEYWORDS = ["confirmed", "approved", "continue", "done", "let's proceed", "looks good", "lgtm"];
3199
3489
  var VERDICT_APPROVE_RE = /\[\s*APPROVE\s*\]/i;
3200
3490
  var VERDICT_REJECT_RE = /\[\s*REJECT\s*\]/i;
@@ -3246,7 +3536,7 @@ function checkPlanCreated(context) {
3246
3536
  summary: `Plan created at ${matchingPlan}`
3247
3537
  };
3248
3538
  }
3249
- const directPath = join9(directory, ".weave", "plans", `${planName}.md`);
3539
+ const directPath = join10(directory, ".weave", "plans", `${planName}.md`);
3250
3540
  if (existsSync11(directPath)) {
3251
3541
  return {
3252
3542
  complete: true,
@@ -3262,7 +3552,7 @@ function checkPlanComplete(context) {
3262
3552
  if (!planName) {
3263
3553
  return { complete: false, reason: "plan_complete requires plan_name in completion config" };
3264
3554
  }
3265
- const planPath = join9(directory, ".weave", "plans", `${planName}.md`);
3555
+ const planPath = join10(directory, ".weave", "plans", `${planName}.md`);
3266
3556
  if (!existsSync11(planPath)) {
3267
3557
  return { complete: false, reason: `Plan file not found: ${planPath}` };
3268
3558
  }
@@ -4224,8 +4514,8 @@ function clearSession2(sessionId) {
4224
4514
  sessionMap.delete(sessionId);
4225
4515
  }
4226
4516
  // src/features/analytics/storage.ts
4227
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, appendFileSync as appendFileSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync3, statSync as statSync2 } from "fs";
4228
- import { join as join10 } from "path";
4517
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, appendFileSync as appendFileSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync4, statSync as statSync3 } from "fs";
4518
+ import { join as join11 } from "path";
4229
4519
 
4230
4520
  // src/features/analytics/types.ts
4231
4521
  var ANALYTICS_DIR = ".weave/analytics";
@@ -4240,30 +4530,30 @@ function zeroTokenUsage() {
4240
4530
  // src/features/analytics/storage.ts
4241
4531
  var MAX_SESSION_ENTRIES = 1000;
4242
4532
  function ensureAnalyticsDir(directory) {
4243
- const dir = join10(directory, ANALYTICS_DIR);
4244
- mkdirSync4(dir, { recursive: true, mode: 448 });
4533
+ const dir = join11(directory, ANALYTICS_DIR);
4534
+ mkdirSync5(dir, { recursive: true, mode: 448 });
4245
4535
  return dir;
4246
4536
  }
4247
4537
  function appendSessionSummary(directory, summary) {
4248
4538
  try {
4249
4539
  const dir = ensureAnalyticsDir(directory);
4250
- const filePath = join10(dir, SESSION_SUMMARIES_FILE);
4540
+ const filePath = join11(dir, SESSION_SUMMARIES_FILE);
4251
4541
  const line = JSON.stringify(summary) + `
4252
4542
  `;
4253
4543
  appendFileSync2(filePath, line, { encoding: "utf-8", mode: 384 });
4254
4544
  try {
4255
4545
  const TYPICAL_ENTRY_BYTES = 200;
4256
4546
  const rotationSizeThreshold = MAX_SESSION_ENTRIES * TYPICAL_ENTRY_BYTES * 0.9;
4257
- const { size } = statSync2(filePath);
4547
+ const { size } = statSync3(filePath);
4258
4548
  if (size > rotationSizeThreshold) {
4259
- const content = readFileSync9(filePath, "utf-8");
4549
+ const content = readFileSync10(filePath, "utf-8");
4260
4550
  const lines = content.split(`
4261
4551
  `).filter((l) => l.trim().length > 0);
4262
4552
  if (lines.length > MAX_SESSION_ENTRIES) {
4263
4553
  const trimmed = lines.slice(-MAX_SESSION_ENTRIES).join(`
4264
4554
  `) + `
4265
4555
  `;
4266
- writeFileSync3(filePath, trimmed, { encoding: "utf-8", mode: 384 });
4556
+ writeFileSync4(filePath, trimmed, { encoding: "utf-8", mode: 384 });
4267
4557
  }
4268
4558
  }
4269
4559
  } catch {}
@@ -4273,11 +4563,11 @@ function appendSessionSummary(directory, summary) {
4273
4563
  }
4274
4564
  }
4275
4565
  function readSessionSummaries(directory) {
4276
- const filePath = join10(directory, ANALYTICS_DIR, SESSION_SUMMARIES_FILE);
4566
+ const filePath = join11(directory, ANALYTICS_DIR, SESSION_SUMMARIES_FILE);
4277
4567
  try {
4278
4568
  if (!existsSync12(filePath))
4279
4569
  return [];
4280
- const content = readFileSync9(filePath, "utf-8");
4570
+ const content = readFileSync10(filePath, "utf-8");
4281
4571
  const lines = content.split(`
4282
4572
  `).filter((line) => line.trim().length > 0);
4283
4573
  const summaries = [];
@@ -4294,19 +4584,19 @@ function readSessionSummaries(directory) {
4294
4584
  function writeFingerprint(directory, fingerprint) {
4295
4585
  try {
4296
4586
  const dir = ensureAnalyticsDir(directory);
4297
- const filePath = join10(dir, FINGERPRINT_FILE);
4298
- writeFileSync3(filePath, JSON.stringify(fingerprint, null, 2), { encoding: "utf-8", mode: 384 });
4587
+ const filePath = join11(dir, FINGERPRINT_FILE);
4588
+ writeFileSync4(filePath, JSON.stringify(fingerprint, null, 2), { encoding: "utf-8", mode: 384 });
4299
4589
  return true;
4300
4590
  } catch {
4301
4591
  return false;
4302
4592
  }
4303
4593
  }
4304
4594
  function readFingerprint(directory) {
4305
- const filePath = join10(directory, ANALYTICS_DIR, FINGERPRINT_FILE);
4595
+ const filePath = join11(directory, ANALYTICS_DIR, FINGERPRINT_FILE);
4306
4596
  try {
4307
4597
  if (!existsSync12(filePath))
4308
4598
  return null;
4309
- const content = readFileSync9(filePath, "utf-8");
4599
+ const content = readFileSync10(filePath, "utf-8");
4310
4600
  const parsed = JSON.parse(content);
4311
4601
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.stack))
4312
4602
  return null;
@@ -4318,23 +4608,23 @@ function readFingerprint(directory) {
4318
4608
  function writeMetricsReport(directory, report) {
4319
4609
  try {
4320
4610
  const dir = ensureAnalyticsDir(directory);
4321
- const filePath = join10(dir, METRICS_REPORTS_FILE);
4611
+ const filePath = join11(dir, METRICS_REPORTS_FILE);
4322
4612
  const line = JSON.stringify(report) + `
4323
4613
  `;
4324
4614
  appendFileSync2(filePath, line, { encoding: "utf-8", mode: 384 });
4325
4615
  try {
4326
4616
  const TYPICAL_ENTRY_BYTES = 200;
4327
4617
  const rotationSizeThreshold = MAX_METRICS_ENTRIES * TYPICAL_ENTRY_BYTES * 0.9;
4328
- const { size } = statSync2(filePath);
4618
+ const { size } = statSync3(filePath);
4329
4619
  if (size > rotationSizeThreshold) {
4330
- const content = readFileSync9(filePath, "utf-8");
4620
+ const content = readFileSync10(filePath, "utf-8");
4331
4621
  const lines = content.split(`
4332
4622
  `).filter((l) => l.trim().length > 0);
4333
4623
  if (lines.length > MAX_METRICS_ENTRIES) {
4334
4624
  const trimmed = lines.slice(-MAX_METRICS_ENTRIES).join(`
4335
4625
  `) + `
4336
4626
  `;
4337
- writeFileSync3(filePath, trimmed, { encoding: "utf-8", mode: 384 });
4627
+ writeFileSync4(filePath, trimmed, { encoding: "utf-8", mode: 384 });
4338
4628
  }
4339
4629
  }
4340
4630
  } catch {}
@@ -4344,11 +4634,11 @@ function writeMetricsReport(directory, report) {
4344
4634
  }
4345
4635
  }
4346
4636
  function readMetricsReports(directory) {
4347
- const filePath = join10(directory, ANALYTICS_DIR, METRICS_REPORTS_FILE);
4637
+ const filePath = join11(directory, ANALYTICS_DIR, METRICS_REPORTS_FILE);
4348
4638
  try {
4349
4639
  if (!existsSync12(filePath))
4350
4640
  return [];
4351
- const content = readFileSync9(filePath, "utf-8");
4641
+ const content = readFileSync10(filePath, "utf-8");
4352
4642
  const lines = content.split(`
4353
4643
  `).filter((line) => line.trim().length > 0);
4354
4644
  const reports = [];
@@ -4509,7 +4799,7 @@ function topTools(summaries, limit = 5) {
4509
4799
  counts[t.tool] = (counts[t.tool] ?? 0) + t.count;
4510
4800
  }
4511
4801
  }
4512
- return Object.entries(counts).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count).slice(0, limit);
4802
+ return Object.entries(counts).map(([tool4, count]) => ({ tool: tool4, count })).sort((a, b) => b.count - a.count).slice(0, limit);
4513
4803
  }
4514
4804
  function formatMetricsMarkdown(reports, summaries, args) {
4515
4805
  if (reports.length === 0 && summaries.length === 0) {
@@ -4574,7 +4864,7 @@ function formatMetricsMarkdown(reports, summaries, args) {
4574
4864
  }
4575
4865
 
4576
4866
  // src/features/analytics/plan-parser.ts
4577
- import { readFileSync as readFileSync10 } from "fs";
4867
+ import { readFileSync as readFileSync11 } from "fs";
4578
4868
  function extractSection2(content, heading) {
4579
4869
  const lines = content.split(`
4580
4870
  `);
@@ -4609,7 +4899,7 @@ function extractFilePath2(raw) {
4609
4899
  function extractPlannedFiles(planPath) {
4610
4900
  let content;
4611
4901
  try {
4612
- content = readFileSync10(planPath, "utf-8");
4902
+ content = readFileSync11(planPath, "utf-8");
4613
4903
  } catch {
4614
4904
  return [];
4615
4905
  }
@@ -4759,7 +5049,7 @@ function generateMetricsReport(directory, state) {
4759
5049
  // src/plugin/plugin-interface.ts
4760
5050
  var FINALIZE_TODOS_MARKER = "<!-- weave:finalize-todos -->";
4761
5051
  function createPluginInterface(args) {
4762
- const { pluginConfig, hooks, tools, configHandler, agents, client, directory = "", tracker } = args;
5052
+ const { pluginConfig, hooks, tools, configHandler, agents, client, directory = "", tracker, taskSystemEnabled = false } = args;
4763
5053
  const lastAssistantMessageText = new Map;
4764
5054
  const lastUserMessageText = new Map;
4765
5055
  const todoFinalizedSessions = new Set;
@@ -4771,9 +5061,24 @@ function createPluginInterface(args) {
4771
5061
  agents,
4772
5062
  availableTools: []
4773
5063
  });
4774
- config.agent = result.agents;
4775
- config.command = result.commands;
4776
- if (result.defaultAgent) {
5064
+ const existingAgents = config.agent ?? {};
5065
+ if (Object.keys(existingAgents).length > 0) {
5066
+ log("[config] Merging Weave agents over existing agents", {
5067
+ existingCount: Object.keys(existingAgents).length,
5068
+ weaveCount: Object.keys(result.agents).length,
5069
+ existingKeys: Object.keys(existingAgents)
5070
+ });
5071
+ const collisions = Object.keys(result.agents).filter((key) => (key in existingAgents));
5072
+ if (collisions.length > 0) {
5073
+ log("[config] Weave agents overriding user-defined agents with same name", {
5074
+ overriddenKeys: collisions
5075
+ });
5076
+ }
5077
+ }
5078
+ config.agent = { ...existingAgents, ...result.agents };
5079
+ const existingCommands = config.command ?? {};
5080
+ config.command = { ...existingCommands, ...result.commands };
5081
+ if (result.defaultAgent && !config.default_agent) {
4777
5082
  config.default_agent = result.defaultAgent;
4778
5083
  }
4779
5084
  },
@@ -4845,7 +5150,7 @@ ${result.contextInjection}`;
4845
5150
  `).trim() ?? "";
4846
5151
  if (userText && sessionID) {
4847
5152
  lastUserMessageText.set(sessionID, userText);
4848
- if (!userText.includes(FINALIZE_TODOS_MARKER)) {
5153
+ if (!taskSystemEnabled && !userText.includes(FINALIZE_TODOS_MARKER)) {
4849
5154
  todoFinalizedSessions.delete(sessionID);
4850
5155
  }
4851
5156
  }
@@ -4882,7 +5187,7 @@ ${cmdResult.contextInjection}`;
4882
5187
  const isStartWork = promptText.includes("<session-context>");
4883
5188
  const isContinuation = promptText.includes(CONTINUATION_MARKER);
4884
5189
  const isWorkflowContinuation = promptText.includes(WORKFLOW_CONTINUATION_MARKER);
4885
- const isTodoFinalize = promptText.includes(FINALIZE_TODOS_MARKER);
5190
+ const isTodoFinalize = !taskSystemEnabled && promptText.includes(FINALIZE_TODOS_MARKER);
4886
5191
  const isActiveWorkflow = (() => {
4887
5192
  const wf = getActiveWorkflowInstance(directory);
4888
5193
  return wf != null && wf.status === "running";
@@ -5066,7 +5371,7 @@ ${cmdResult.contextInjection}`;
5066
5371
  }
5067
5372
  }
5068
5373
  }
5069
- if (event.type === "session.idle" && client && !continuationFired) {
5374
+ if (event.type === "session.idle" && client && !continuationFired && !taskSystemEnabled) {
5070
5375
  const evt = event;
5071
5376
  const sessionId = evt.properties?.sessionID ?? "";
5072
5377
  if (sessionId && !todoFinalizedSessions.has(sessionId)) {
@@ -5182,14 +5487,14 @@ Use todowrite NOW to mark all of them as "completed" (or "cancelled" if abandone
5182
5487
  };
5183
5488
  }
5184
5489
  // src/features/analytics/fingerprint.ts
5185
- import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync5 } from "fs";
5186
- import { join as join12 } from "path";
5490
+ import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync6 } from "fs";
5491
+ import { join as join13 } from "path";
5187
5492
  import { arch } from "os";
5188
5493
 
5189
5494
  // src/shared/version.ts
5190
- import { readFileSync as readFileSync11 } from "fs";
5495
+ import { readFileSync as readFileSync12 } from "fs";
5191
5496
  import { fileURLToPath } from "url";
5192
- import { dirname as dirname2, join as join11 } from "path";
5497
+ import { dirname as dirname2, join as join12 } from "path";
5193
5498
  var cachedVersion;
5194
5499
  function getWeaveVersion() {
5195
5500
  if (cachedVersion !== undefined)
@@ -5198,7 +5503,7 @@ function getWeaveVersion() {
5198
5503
  const thisDir = dirname2(fileURLToPath(import.meta.url));
5199
5504
  for (const rel of ["../../package.json", "../package.json"]) {
5200
5505
  try {
5201
- const pkg = JSON.parse(readFileSync11(join11(thisDir, rel), "utf-8"));
5506
+ const pkg = JSON.parse(readFileSync12(join12(thisDir, rel), "utf-8"));
5202
5507
  if (pkg.name === "@opencode_weave/weave" && typeof pkg.version === "string") {
5203
5508
  const version = pkg.version;
5204
5509
  cachedVersion = version;
@@ -5303,7 +5608,7 @@ function detectStack(directory) {
5303
5608
  const detected = [];
5304
5609
  for (const marker of STACK_MARKERS) {
5305
5610
  for (const file of marker.files) {
5306
- if (existsSync13(join12(directory, file))) {
5611
+ if (existsSync13(join13(directory, file))) {
5307
5612
  detected.push({
5308
5613
  name: marker.name,
5309
5614
  confidence: marker.confidence,
@@ -5314,9 +5619,9 @@ function detectStack(directory) {
5314
5619
  }
5315
5620
  }
5316
5621
  try {
5317
- const pkgPath = join12(directory, "package.json");
5622
+ const pkgPath = join13(directory, "package.json");
5318
5623
  if (existsSync13(pkgPath)) {
5319
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
5624
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
5320
5625
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
5321
5626
  if (deps.react) {
5322
5627
  detected.push({
@@ -5329,7 +5634,7 @@ function detectStack(directory) {
5329
5634
  } catch {}
5330
5635
  if (!detected.some((d) => d.name === "dotnet")) {
5331
5636
  try {
5332
- const entries = readdirSync5(directory);
5637
+ const entries = readdirSync6(directory);
5333
5638
  const dotnetFile = entries.find((e) => e.endsWith(".csproj") || e.endsWith(".fsproj") || e.endsWith(".sln"));
5334
5639
  if (dotnetFile) {
5335
5640
  detected.push({
@@ -5349,27 +5654,27 @@ function detectStack(directory) {
5349
5654
  });
5350
5655
  }
5351
5656
  function detectPackageManager(directory) {
5352
- if (existsSync13(join12(directory, "bun.lockb")))
5657
+ if (existsSync13(join13(directory, "bun.lockb")))
5353
5658
  return "bun";
5354
- if (existsSync13(join12(directory, "pnpm-lock.yaml")))
5659
+ if (existsSync13(join13(directory, "pnpm-lock.yaml")))
5355
5660
  return "pnpm";
5356
- if (existsSync13(join12(directory, "yarn.lock")))
5661
+ if (existsSync13(join13(directory, "yarn.lock")))
5357
5662
  return "yarn";
5358
- if (existsSync13(join12(directory, "package-lock.json")))
5663
+ if (existsSync13(join13(directory, "package-lock.json")))
5359
5664
  return "npm";
5360
- if (existsSync13(join12(directory, "package.json")))
5665
+ if (existsSync13(join13(directory, "package.json")))
5361
5666
  return "npm";
5362
5667
  return;
5363
5668
  }
5364
5669
  function detectMonorepo(directory) {
5365
5670
  for (const marker of MONOREPO_MARKERS) {
5366
- if (existsSync13(join12(directory, marker)))
5671
+ if (existsSync13(join13(directory, marker)))
5367
5672
  return true;
5368
5673
  }
5369
5674
  try {
5370
- const pkgPath = join12(directory, "package.json");
5675
+ const pkgPath = join13(directory, "package.json");
5371
5676
  if (existsSync13(pkgPath)) {
5372
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
5677
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
5373
5678
  if (pkg.workspaces)
5374
5679
  return true;
5375
5680
  }
@@ -5527,7 +5832,7 @@ class SessionTracker {
5527
5832
  const now = new Date;
5528
5833
  const startedAt = new Date(session.startedAt);
5529
5834
  const durationMs = now.getTime() - startedAt.getTime();
5530
- const toolUsage = Object.entries(session.toolCounts).map(([tool, count]) => ({ tool, count }));
5835
+ const toolUsage = Object.entries(session.toolCounts).map(([tool4, count]) => ({ tool: tool4, count }));
5531
5836
  const totalToolCalls = toolUsage.reduce((sum, entry) => sum + entry.count, 0);
5532
5837
  const summary = {
5533
5838
  sessionId,
@@ -5589,7 +5894,7 @@ var WeavePlugin = async (ctx) => {
5589
5894
  const analyticsEnabled = pluginConfig.analytics?.enabled === true;
5590
5895
  const fingerprintEnabled = analyticsEnabled && pluginConfig.analytics?.use_fingerprint === true;
5591
5896
  const fingerprint = fingerprintEnabled ? getOrCreateFingerprint(ctx.directory) : null;
5592
- const configDir = join13(ctx.directory, ".opencode");
5897
+ const configDir = join14(ctx.directory, ".opencode");
5593
5898
  const toolsResult = await createTools({ ctx, pluginConfig });
5594
5899
  const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn, fingerprint, configDir });
5595
5900
  const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory, analyticsEnabled });
@@ -5602,7 +5907,8 @@ var WeavePlugin = async (ctx) => {
5602
5907
  agents: managers.agents,
5603
5908
  client: ctx.client,
5604
5909
  directory: ctx.directory,
5605
- tracker: analytics?.tracker
5910
+ tracker: analytics?.tracker,
5911
+ taskSystemEnabled: pluginConfig.experimental?.task_system !== false
5606
5912
  });
5607
5913
  };
5608
5914
  var src_default = WeavePlugin;
@@ -14,4 +14,5 @@ export declare function createPluginInterface(args: {
14
14
  client?: PluginContext["client"];
15
15
  directory?: string;
16
16
  tracker?: SessionTracker;
17
+ taskSystemEnabled?: boolean;
17
18
  }): PluginInterface;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencode_weave/weave",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Weave — lean OpenCode plugin with multi-agent orchestration",
5
5
  "author": "Weave",
6
6
  "license": "MIT",