@mclean-capital/neura 3.5.0 → 3.5.2

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.
@@ -76114,7 +76114,7 @@ var taskToolDefs = [
76114
76114
  {
76115
76115
  type: "function",
76116
76116
  name: "create_task",
76117
- description: "Create a new task. Use when the user asks you to do something actionable (file operations, research, code changes, reminders). Orchestrator uses create_task to brief a worker BEFORE dispatching \u2014 pair with dispatch_worker when the user confirms. Include `goal` whenever possible (what success looks like).",
76117
+ description: "Create a new task. Use when the user asks you to do something actionable (file operations, research, code changes, reminders). Orchestrator uses create_task to brief a worker BEFORE dispatching \u2014 pair with dispatch_worker when the user confirms. Include `goal` whenever possible (what success looks like). The returned `id` is an internal handle for subsequent tool calls \u2014 NEVER speak it aloud; refer to the task by its title when talking to the user.",
76118
76118
  parameters: {
76119
76119
  type: "object",
76120
76120
  properties: {
@@ -76264,7 +76264,7 @@ var taskToolDefs = [
76264
76264
  {
76265
76265
  type: "function",
76266
76266
  name: "dispatch_worker",
76267
- description: "Dispatch a worker to execute an existing task. The task must already have a clear goal + context \u2014 call create_task first, confirm with the user for non-trivial / destructive work, then dispatch. Returns a worker_id immediately; progress flows via comments on the task.",
76267
+ description: "Dispatch a worker to execute an existing task. The task must already have a clear goal + context \u2014 call create_task first, confirm with the user for non-trivial / destructive work, then dispatch. Progress flows via comments on the task (see get_task). When confirming to the user, speak naturally about the task ('Dispatching the worker now') \u2014 do NOT quote task IDs or worker IDs aloud, the TTS reads UUIDs letter by letter and it's jarring.",
76268
76268
  parameters: {
76269
76269
  type: "object",
76270
76270
  properties: {
@@ -76335,7 +76335,16 @@ async function handleTaskTool(name, args, ctx) {
76335
76335
  const query = args.query;
76336
76336
  const task = await ctx.taskTools.getTask(query);
76337
76337
  if (!task) return { result: { found: false } };
76338
- return { result: { found: true, task } };
76338
+ let comments = [];
76339
+ try {
76340
+ comments = await ctx.taskTools.listTaskComments(task.id, { limit: 50 });
76341
+ } catch (err) {
76342
+ log15.warn("failed to load task comments for get_task", {
76343
+ taskId: task.id,
76344
+ err: String(err)
76345
+ });
76346
+ }
76347
+ return { result: { found: true, task, comments } };
76339
76348
  }
76340
76349
  case "update_task": {
76341
76350
  if (!ctx.taskTools) return { error: "Task system not available" };
@@ -76396,8 +76405,7 @@ async function handleTaskTool(name, args, ctx) {
76396
76405
  return {
76397
76406
  result: {
76398
76407
  dispatched: true,
76399
- workerId: outcome.workerId,
76400
- message: `Worker ${outcome.workerId} dispatched. You'll hear progress updates as it works.`
76408
+ message: `Worker dispatched. You'll hear progress updates as it works.`
76401
76409
  }
76402
76410
  };
76403
76411
  }
@@ -77355,6 +77363,77 @@ async function sweepCrashedWorkers(db, fileExists) {
77355
77363
  // src/workers/agent-worker.ts
77356
77364
  init_work_item_queries();
77357
77365
 
77366
+ // src/stores/task-comment-queries.ts
77367
+ init_mappers();
77368
+ import crypto4 from "crypto";
77369
+ var TASK_COMMENT_CONTENT_MAX_BYTES = 32 * 1024;
77370
+ async function insertComment(db, opts) {
77371
+ if (Buffer.byteLength(opts.content, "utf8") > TASK_COMMENT_CONTENT_MAX_BYTES) {
77372
+ throw new Error(
77373
+ `task comment content exceeds ${TASK_COMMENT_CONTENT_MAX_BYTES} bytes; write to disk and pass attachment_path with a summary in content`
77374
+ );
77375
+ }
77376
+ const id = crypto4.randomUUID();
77377
+ const result = await db.query(
77378
+ `INSERT INTO task_comments (
77379
+ id, task_id, type, author, content, attachment_path, urgency, metadata
77380
+ )
77381
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
77382
+ RETURNING *`,
77383
+ [
77384
+ id,
77385
+ opts.taskId,
77386
+ opts.type,
77387
+ opts.author,
77388
+ opts.content,
77389
+ opts.attachmentPath ?? null,
77390
+ opts.urgency ?? null,
77391
+ opts.metadata ? JSON.stringify(opts.metadata) : null
77392
+ ]
77393
+ );
77394
+ return mapTaskComment(result.rows[0]);
77395
+ }
77396
+ async function listComments(db, opts) {
77397
+ const limit2 = opts.limit ?? 500;
77398
+ const filters = ["task_id = $1"];
77399
+ const values = [opts.taskId];
77400
+ let idx = 2;
77401
+ if (opts.type !== void 0) {
77402
+ const types3 = Array.isArray(opts.type) ? opts.type : [opts.type];
77403
+ const placeholders = types3.map(() => `$${idx++}`).join(", ");
77404
+ filters.push(`type IN (${placeholders})`);
77405
+ values.push(...types3);
77406
+ }
77407
+ if (opts.since) {
77408
+ filters.push(`created_at > $${idx++}`);
77409
+ values.push(opts.since);
77410
+ }
77411
+ const result = await db.query(
77412
+ `SELECT * FROM task_comments
77413
+ WHERE ${filters.join(" AND ")}
77414
+ ORDER BY created_at ASC
77415
+ LIMIT $${idx}`,
77416
+ [...values, limit2]
77417
+ );
77418
+ return result.rows.map((r2) => mapTaskComment(r2));
77419
+ }
77420
+ async function countOpenRequests(db, taskId) {
77421
+ const result = await db.query(
77422
+ `SELECT COUNT(*)::TEXT as count FROM task_comments
77423
+ WHERE task_id = $1
77424
+ AND type IN ('clarification_request', 'approval_request')
77425
+ AND id NOT IN (
77426
+ -- requests with a matching response comment are considered resolved
77427
+ SELECT (metadata->>'resolves_comment_id')::text FROM task_comments
77428
+ WHERE task_id = $1
77429
+ AND type IN ('clarification_response', 'approval_response')
77430
+ AND metadata ? 'resolves_comment_id'
77431
+ )`,
77432
+ [taskId]
77433
+ );
77434
+ return Number(result.rows[0]?.count ?? 0);
77435
+ }
77436
+
77358
77437
  // src/workers/worktree-manager.ts
77359
77438
  import { execFileSync } from "node:child_process";
77360
77439
  import {
@@ -77590,7 +77669,15 @@ var CANONICAL_WORKER_SYSTEM_PROMPT = `You are a Neura worker \u2014 a capable en
77590
77669
 
77591
77670
  Your posture: be decisive. You have full tool access \u2014 Read, Write, Edit, Bash \u2014 scoped to an isolated worktree directory (your cwd). Make progress. Don't ask the user to double-check obvious things. Don't propose a plan and wait for approval when the path is clear.
77592
77671
 
77672
+ Your cwd is an isolated sandbox under \`~/.neura/worktrees/<workerId>/\`. It is NOT the user's home directory, desktop, or documents folder. Files you create land in the sandbox, not in user-visible locations. A task whose goal requires writing to the user's Desktop, Documents, or any absolute path outside your cwd cannot be satisfied by writing inside cwd \u2014 that would silently put the file in the wrong place. In that case:
77673
+
77674
+ 1. If the outside path is essential to the task, call \`request_approval\` with a rationale ("task goal requires writing to ~/Desktop, which is outside my sandbox \u2014 authorize access?") and wait. If denied, call \`fail_task\` with \`reason_code: 'impossible'\`.
77675
+ 2. If you can satisfy the goal inside cwd without writing to the user-visible path, do that and make it clear in your \`complete_task\` summary where the file actually lives.
77676
+
77677
+ Do NOT pretend success when you wrote to the wrong place. Silent "it completed" with no file where the user expects it is the worst outcome.
77678
+
77593
77679
  Reversibility rule: before any destructive or hard-to-reverse action **outside** your worktree, call \`request_approval\` and wait for the user's answer. Examples that require approval:
77680
+ - Writing to the user's home directory, Desktop, Documents, or any absolute path outside cwd
77594
77681
  - Deleting files the user owns
77595
77682
  - Force-pushing branches, rewriting git history
77596
77683
  - Sending email, SMS, or other external messages
@@ -78030,6 +78117,31 @@ var AgentWorker = class {
78030
78117
  });
78031
78118
  }
78032
78119
  }
78120
+ if ((status === "failed" || status === "crashed") && result.error) {
78121
+ try {
78122
+ const { reason, detail } = result.error;
78123
+ const MAX_DETAIL_BYTES = 3e4;
78124
+ const safeDetail = detail ? detail.slice(0, MAX_DETAIL_BYTES) : "";
78125
+ const content = safeDetail ? `${reason}: ${safeDetail}` : reason;
78126
+ await insertComment(this.db, {
78127
+ taskId,
78128
+ type: "error",
78129
+ author: "system",
78130
+ content,
78131
+ metadata: {
78132
+ workerId,
78133
+ workerStatus: status,
78134
+ reason
78135
+ }
78136
+ });
78137
+ } catch (err) {
78138
+ log23.warn("failed to persist error comment on task", {
78139
+ workerId,
78140
+ taskId,
78141
+ err: String(err)
78142
+ });
78143
+ }
78144
+ }
78033
78145
  }
78034
78146
  try {
78035
78147
  if (status === "completed" || status === "cancelled") {
@@ -78195,53 +78307,6 @@ function buildClarificationTool(workerId, bridge) {
78195
78307
  return tool;
78196
78308
  }
78197
78309
 
78198
- // src/stores/task-comment-queries.ts
78199
- init_mappers();
78200
- import crypto4 from "crypto";
78201
- var TASK_COMMENT_CONTENT_MAX_BYTES = 32 * 1024;
78202
- async function insertComment(db, opts) {
78203
- if (Buffer.byteLength(opts.content, "utf8") > TASK_COMMENT_CONTENT_MAX_BYTES) {
78204
- throw new Error(
78205
- `task comment content exceeds ${TASK_COMMENT_CONTENT_MAX_BYTES} bytes; write to disk and pass attachment_path with a summary in content`
78206
- );
78207
- }
78208
- const id = crypto4.randomUUID();
78209
- const result = await db.query(
78210
- `INSERT INTO task_comments (
78211
- id, task_id, type, author, content, attachment_path, urgency, metadata
78212
- )
78213
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
78214
- RETURNING *`,
78215
- [
78216
- id,
78217
- opts.taskId,
78218
- opts.type,
78219
- opts.author,
78220
- opts.content,
78221
- opts.attachmentPath ?? null,
78222
- opts.urgency ?? null,
78223
- opts.metadata ? JSON.stringify(opts.metadata) : null
78224
- ]
78225
- );
78226
- return mapTaskComment(result.rows[0]);
78227
- }
78228
- async function countOpenRequests(db, taskId) {
78229
- const result = await db.query(
78230
- `SELECT COUNT(*)::TEXT as count FROM task_comments
78231
- WHERE task_id = $1
78232
- AND type IN ('clarification_request', 'approval_request')
78233
- AND id NOT IN (
78234
- -- requests with a matching response comment are considered resolved
78235
- SELECT (metadata->>'resolves_comment_id')::text FROM task_comments
78236
- WHERE task_id = $1
78237
- AND type IN ('clarification_response', 'approval_response')
78238
- AND metadata ? 'resolves_comment_id'
78239
- )`,
78240
- [taskId]
78241
- );
78242
- return Number(result.rows[0]?.count ?? 0);
78243
- }
78244
-
78245
78310
  // src/tools/task-update-handler.ts
78246
78311
  init_work_item_queries();
78247
78312
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "cancelled", "failed"]);
@@ -78672,6 +78737,21 @@ Reason: ${params.rationale}` : params.action;
78672
78737
  return tools;
78673
78738
  }
78674
78739
 
78740
+ // src/server/lifecycle.ts
78741
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
78742
+
78743
+ // src/workers/auth-bridge.ts
78744
+ function seedAuthStorageFromConfig(authStorage, providers) {
78745
+ let count = 0;
78746
+ for (const [providerId, creds] of Object.entries(providers)) {
78747
+ if (creds?.apiKey) {
78748
+ authStorage.setRuntimeApiKey(providerId, creds.apiKey);
78749
+ count++;
78750
+ }
78751
+ }
78752
+ return count;
78753
+ }
78754
+
78675
78755
  // src/tools/vision-tools.ts
78676
78756
  var visionToolDefs = [
78677
78757
  {
@@ -79266,6 +79346,9 @@ async function initServices() {
79266
79346
  return candidates.slice(0, limit2);
79267
79347
  },
79268
79348
  getTask: (idOrTitle) => store.getWorkItem(idOrTitle),
79349
+ listTaskComments: async (taskId, options) => {
79350
+ return listComments(rawDb, { taskId, limit: options?.limit });
79351
+ },
79269
79352
  updateTask: async (idOrTitle, payload) => {
79270
79353
  const current = await store.getWorkItem(idOrTitle);
79271
79354
  if (!current) return null;
@@ -79332,6 +79415,9 @@ async function initServices() {
79332
79415
  `${wProvider}/${wModel} model not registered in pi-ai. Check config.routing.worker or verify pi-ai supports this provider.`
79333
79416
  );
79334
79417
  }
79418
+ const authStorage = AuthStorage.create(join4(agentDir, "auth.json"));
79419
+ const seededProviders = seedAuthStorageFromConfig(authStorage, config2.providers);
79420
+ log29.info("seeded pi auth storage from config", { count: seededProviders });
79335
79421
  const piRuntime = new PiRuntime({
79336
79422
  model,
79337
79423
  thinkingLevel: "low",
@@ -79340,7 +79426,8 @@ async function initServices() {
79340
79426
  sessionDir,
79341
79427
  buildTools,
79342
79428
  voiceFanoutBridge,
79343
- skillRegistry
79429
+ skillRegistry,
79430
+ authStorage
79344
79431
  });
79345
79432
  agentWorker = new AgentWorker({ db: rawDb, runtime: piRuntime });
79346
79433
  await agentWorker.recoverFromCrash();
@@ -98971,6 +99058,11 @@ function attachWebSocket(httpServer2, services2) {
98971
99058
  return candidates.slice(0, limit2);
98972
99059
  },
98973
99060
  getTask: (idOrTitle) => resolveTask(store, idOrTitle),
99061
+ listTaskComments: async (taskId, options) => {
99062
+ const db = store.getRawDb?.();
99063
+ if (!db) throw new Error("store does not expose a raw PGlite handle");
99064
+ return listComments(db, { taskId, limit: options?.limit });
99065
+ },
98974
99066
  updateTask: async (idOrTitle, payload) => {
98975
99067
  const current = await resolveTask(store, idOrTitle);
98976
99068
  if (!current) return null;