@runfusion/fusion 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +2 -0
  2. package/dist/bin.js +4055 -1755
  3. package/dist/client/assets/AgentDetailView-CDZED6Dy.css +1 -0
  4. package/dist/client/assets/AgentDetailView-zycSdnO8.js +28 -0
  5. package/dist/client/assets/AgentsView-DoQkkDLf.css +1 -0
  6. package/dist/client/assets/AgentsView-pO7WiBS5.js +522 -0
  7. package/dist/client/assets/ChatView-BOd-sxbT.js +1 -0
  8. package/dist/client/assets/DevServerView-09GQf34f.js +11 -0
  9. package/dist/client/assets/DevServerView-ZeBGQkLI.css +1 -0
  10. package/dist/client/assets/DirectoryPicker-CcdN1Zs7.js +1 -0
  11. package/dist/client/assets/DocumentsView-CS8aiwtz.js +1 -0
  12. package/dist/client/assets/DocumentsView-Co9to4Zp.css +1 -0
  13. package/dist/client/assets/InsightsView-Bu9Cv8Ol.js +11 -0
  14. package/dist/client/assets/InsightsView-Egu71gmh.css +1 -0
  15. package/dist/client/assets/MemoryView-CtqgDtV9.js +2 -0
  16. package/dist/client/assets/MemoryView-DhinauGs.css +1 -0
  17. package/dist/client/assets/NodesView-BInPcedy.js +14 -0
  18. package/dist/client/assets/NodesView-DlQZHGXA.css +1 -0
  19. package/dist/client/assets/PiExtensionsManager-COxkYM2m.js +11 -0
  20. package/dist/client/assets/PiExtensionsManager-CPgmJgDk.css +1 -0
  21. package/dist/client/assets/PluginManager-CXUWZBOc.js +1 -0
  22. package/dist/client/assets/PluginManager-D64RIzmL.css +1 -0
  23. package/dist/client/assets/RoadmapsView-BOYnyMCh.css +1 -0
  24. package/dist/client/assets/RoadmapsView-BbCexaoi.js +6 -0
  25. package/dist/client/assets/SetupWizardModal-Cakxqkad.js +1 -0
  26. package/dist/client/assets/SkillsView-Cytf009Z.css +1 -0
  27. package/dist/client/assets/SkillsView-D3iqYCVf.js +1 -0
  28. package/dist/client/assets/folder-open-kO5Hsk66.js +6 -0
  29. package/dist/client/assets/index-BiSuUXCa.css +1 -0
  30. package/dist/client/assets/index-y194HxzU.js +644 -0
  31. package/dist/client/assets/upload-DHBQat92.js +6 -0
  32. package/dist/client/index.html +2 -2
  33. package/dist/client/sw.js +45 -1
  34. package/dist/client/theme-data.css +109 -0
  35. package/dist/extension.js +969 -408
  36. package/dist/pi-claude-cli/index.ts +131 -0
  37. package/dist/pi-claude-cli/package.json +39 -0
  38. package/dist/pi-claude-cli/src/__tests__/control-handler.test.ts +191 -0
  39. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +1244 -0
  40. package/dist/pi-claude-cli/src/__tests__/mcp-config.test.ts +272 -0
  41. package/dist/pi-claude-cli/src/__tests__/process-manager.test.ts +619 -0
  42. package/dist/pi-claude-cli/src/__tests__/prompt-builder.test.ts +1067 -0
  43. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +1902 -0
  44. package/dist/pi-claude-cli/src/__tests__/stream-parser.test.ts +188 -0
  45. package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +141 -0
  46. package/dist/pi-claude-cli/src/__tests__/tool-mapping.test.ts +252 -0
  47. package/dist/pi-claude-cli/src/control-handler.ts +68 -0
  48. package/dist/pi-claude-cli/src/event-bridge.ts +386 -0
  49. package/dist/pi-claude-cli/src/mcp-config.ts +111 -0
  50. package/dist/pi-claude-cli/src/mcp-schema-server.cjs +49 -0
  51. package/dist/pi-claude-cli/src/process-manager.ts +218 -0
  52. package/dist/pi-claude-cli/src/prompt-builder.ts +536 -0
  53. package/dist/pi-claude-cli/src/provider.ts +354 -0
  54. package/dist/pi-claude-cli/src/stream-parser.ts +37 -0
  55. package/dist/pi-claude-cli/src/thinking-config.ts +83 -0
  56. package/dist/pi-claude-cli/src/tool-mapping.ts +147 -0
  57. package/dist/pi-claude-cli/src/types.ts +87 -0
  58. package/package.json +11 -4
  59. package/skill/fusion/SKILL.md +5 -3
  60. package/skill/fusion/references/cli-commands.md +22 -22
  61. package/skill/fusion/references/extension-tools.md +3 -1
  62. package/skill/fusion/references/fusion-capabilities.md +28 -35
  63. package/skill/fusion/references/task-structure.md +4 -4
  64. package/skill/fusion/workflows/dashboard-cli.md +6 -6
  65. package/skill/fusion/workflows/specifications.md +5 -3
  66. package/skill/fusion/workflows/task-lifecycle.md +1 -1
  67. package/skill/fusion/workflows/task-management.md +3 -1
  68. package/dist/client/assets/index-Djv5vKo0.css +0 -1
  69. package/dist/client/assets/index-zfXYuUXG.js +0 -1241
package/dist/extension.js CHANGED
@@ -60,6 +60,7 @@ var init_settings_schema = __esm({
60
60
  defaultThinkingLevel: void 0,
61
61
  ntfyEnabled: false,
62
62
  ntfyTopic: void 0,
63
+ ntfyBaseUrl: void 0,
63
64
  ntfyEvents: ["in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "planning-awaiting-input"],
64
65
  ntfyDashboardHost: void 0,
65
66
  defaultProjectId: void 0,
@@ -767,7 +768,7 @@ function validateMessageMetadata(metadata) {
767
768
  throw new Error("metadata.replyTo.messageId must be a non-empty string");
768
769
  }
769
770
  }
770
- var THINKING_LEVELS, COLUMNS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, CheckoutConflictError, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS;
771
+ var THINKING_LEVELS, COLUMNS, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, CheckoutConflictError, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS;
771
772
  var init_types = __esm({
772
773
  "../core/src/types.ts"() {
773
774
  "use strict";
@@ -776,6 +777,8 @@ var init_types = __esm({
776
777
  init_error_message();
777
778
  THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
778
779
  COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
780
+ TASK_PRIORITIES = ["low", "normal", "high", "urgent"];
781
+ DEFAULT_TASK_PRIORITY = "normal";
779
782
  EXECUTION_MODES = ["standard", "fast"];
780
783
  DEFAULT_EXECUTION_MODE = "standard";
781
784
  THEME_MODES = ["dark", "light", "system"];
@@ -2069,13 +2072,14 @@ var init_db = __esm({
2069
2072
  "../core/src/db.ts"() {
2070
2073
  "use strict";
2071
2074
  init_types();
2072
- SCHEMA_VERSION = 42;
2075
+ SCHEMA_VERSION = 45;
2073
2076
  SCHEMA_SQL = `
2074
2077
  -- Tasks table with JSON columns for nested data
2075
2078
  CREATE TABLE IF NOT EXISTS tasks (
2076
2079
  id TEXT PRIMARY KEY,
2077
2080
  title TEXT,
2078
2081
  description TEXT NOT NULL,
2082
+ priority TEXT DEFAULT 'normal',
2079
2083
  "column" TEXT NOT NULL,
2080
2084
  status TEXT,
2081
2085
  size TEXT,
@@ -2103,6 +2107,12 @@ CREATE TABLE IF NOT EXISTS tasks (
2103
2107
  summary TEXT,
2104
2108
  thinkingLevel TEXT,
2105
2109
  executionMode TEXT DEFAULT 'standard',
2110
+ tokenUsageInputTokens INTEGER,
2111
+ tokenUsageOutputTokens INTEGER,
2112
+ tokenUsageCachedTokens INTEGER,
2113
+ tokenUsageTotalTokens INTEGER,
2114
+ tokenUsageFirstUsedAt TEXT,
2115
+ tokenUsageLastUsedAt TEXT,
2106
2116
  createdAt TEXT NOT NULL,
2107
2117
  updatedAt TEXT NOT NULL,
2108
2118
  columnMovedAt TEXT,
@@ -2116,6 +2126,11 @@ CREATE TABLE IF NOT EXISTS tasks (
2116
2126
  workflowStepResults TEXT DEFAULT '[]',
2117
2127
  prInfo TEXT,
2118
2128
  issueInfo TEXT,
2129
+ sourceIssueProvider TEXT,
2130
+ sourceIssueRepository TEXT,
2131
+ sourceIssueExternalIssueId TEXT,
2132
+ sourceIssueNumber INTEGER,
2133
+ sourceIssueUrl TEXT,
2119
2134
  mergeDetails TEXT,
2120
2135
  breakIntoSubtasks INTEGER DEFAULT 0,
2121
2136
  enabledWorkflowSteps TEXT DEFAULT '[]',
@@ -3410,6 +3425,35 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
3410
3425
  `);
3411
3426
  });
3412
3427
  }
3428
+ if (version < 43) {
3429
+ this.applyMigration(43, () => {
3430
+ this.addColumnIfMissing("tasks", "priority", "TEXT DEFAULT 'normal'");
3431
+ this.db.exec(`
3432
+ UPDATE tasks
3433
+ SET priority = 'normal'
3434
+ WHERE priority IS NULL OR priority = '' OR priority NOT IN ('low', 'normal', 'high', 'urgent')
3435
+ `);
3436
+ });
3437
+ }
3438
+ if (version < 44) {
3439
+ this.applyMigration(44, () => {
3440
+ this.addColumnIfMissing("tasks", "tokenUsageInputTokens", "INTEGER");
3441
+ this.addColumnIfMissing("tasks", "tokenUsageOutputTokens", "INTEGER");
3442
+ this.addColumnIfMissing("tasks", "tokenUsageCachedTokens", "INTEGER");
3443
+ this.addColumnIfMissing("tasks", "tokenUsageTotalTokens", "INTEGER");
3444
+ this.addColumnIfMissing("tasks", "tokenUsageFirstUsedAt", "TEXT");
3445
+ this.addColumnIfMissing("tasks", "tokenUsageLastUsedAt", "TEXT");
3446
+ });
3447
+ }
3448
+ if (version < 45) {
3449
+ this.applyMigration(45, () => {
3450
+ this.addColumnIfMissing("tasks", "sourceIssueProvider", "TEXT");
3451
+ this.addColumnIfMissing("tasks", "sourceIssueRepository", "TEXT");
3452
+ this.addColumnIfMissing("tasks", "sourceIssueExternalIssueId", "TEXT");
3453
+ this.addColumnIfMissing("tasks", "sourceIssueNumber", "INTEGER");
3454
+ this.addColumnIfMissing("tasks", "sourceIssueUrl", "TEXT");
3455
+ });
3456
+ }
3413
3457
  }
3414
3458
  /**
3415
3459
  * Run a single migration step inside a transaction and bump the version.
@@ -5680,6 +5724,54 @@ var init_message_store = __esm({
5680
5724
  }
5681
5725
  });
5682
5726
 
5727
+ // ../core/src/task-priority.ts
5728
+ function isTaskPriority(value) {
5729
+ return typeof value === "string" && TASK_PRIORITIES.includes(value);
5730
+ }
5731
+ function normalizeTaskPriority(priority) {
5732
+ return isTaskPriority(priority) ? priority : DEFAULT_TASK_PRIORITY;
5733
+ }
5734
+ function getTaskPriorityRank(priority) {
5735
+ return PRIORITY_RANK[normalizeTaskPriority(priority)];
5736
+ }
5737
+ function compareTaskPriority(a, b) {
5738
+ return getTaskPriorityRank(b) - getTaskPriorityRank(a);
5739
+ }
5740
+ function compareTaskId(a, b) {
5741
+ const aNum = Number.parseInt(a.slice(a.lastIndexOf("-") + 1), 10);
5742
+ const bNum = Number.parseInt(b.slice(b.lastIndexOf("-") + 1), 10);
5743
+ if (Number.isFinite(aNum) && Number.isFinite(bNum) && aNum !== bNum) {
5744
+ return aNum - bNum;
5745
+ }
5746
+ return a.localeCompare(b);
5747
+ }
5748
+ function compareTasksByPriorityThenAgeAndId(a, b) {
5749
+ const priorityCmp = compareTaskPriority(a.priority, b.priority);
5750
+ if (priorityCmp !== 0) {
5751
+ return priorityCmp;
5752
+ }
5753
+ if (a.createdAt !== b.createdAt) {
5754
+ return a.createdAt.localeCompare(b.createdAt);
5755
+ }
5756
+ return compareTaskId(a.id, b.id);
5757
+ }
5758
+ function sortTasksByPriorityThenAgeAndId(tasks) {
5759
+ return [...tasks].sort(compareTasksByPriorityThenAgeAndId);
5760
+ }
5761
+ var PRIORITY_RANK;
5762
+ var init_task_priority = __esm({
5763
+ "../core/src/task-priority.ts"() {
5764
+ "use strict";
5765
+ init_types();
5766
+ PRIORITY_RANK = {
5767
+ low: 0,
5768
+ normal: 1,
5769
+ high: 2,
5770
+ urgent: 3
5771
+ };
5772
+ }
5773
+ });
5774
+
5683
5775
  // ../core/src/global-settings.ts
5684
5776
  import { homedir } from "node:os";
5685
5777
  import { dirname, join as join4 } from "node:path";
@@ -6177,17 +6269,18 @@ async function migrateTasks(fusionDir, db) {
6177
6269
  let skipped = 0;
6178
6270
  const insertStmt = db.prepare(`
6179
6271
  INSERT OR REPLACE INTO tasks (
6180
- id, title, description, "column", status, size, reviewLevel, currentStep,
6272
+ id, title, description, priority, "column", status, size, reviewLevel, currentStep,
6181
6273
  worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId,
6182
6274
  modelProvider, modelId, validatorModelProvider, validatorModelId,
6183
6275
  mergeRetries, recoveryRetryCount, nextRecoveryAt,
6184
6276
  error, summary, thinkingLevel, createdAt, updatedAt,
6185
6277
  columnMovedAt, dependencies, steps, log, attachments, steeringComments,
6186
- comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
6187
- breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
6278
+ comments, workflowStepResults, prInfo, issueInfo,
6279
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
6280
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
6188
6281
  ) VALUES (
6189
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
6190
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
6282
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
6283
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
6191
6284
  )
6192
6285
  `);
6193
6286
  for (const entry of entries) {
@@ -6205,6 +6298,7 @@ async function migrateTasks(fusionDir, db) {
6205
6298
  task.id,
6206
6299
  task.title ?? null,
6207
6300
  task.description,
6301
+ normalizeTaskPriority(task.priority),
6208
6302
  task.column,
6209
6303
  task.status ?? null,
6210
6304
  task.size ?? null,
@@ -6238,6 +6332,11 @@ async function migrateTasks(fusionDir, db) {
6238
6332
  toJson(task.workflowStepResults || []),
6239
6333
  toJsonNullable(task.prInfo),
6240
6334
  toJsonNullable(task.issueInfo),
6335
+ task.sourceIssue?.provider ?? null,
6336
+ task.sourceIssue?.repository ?? null,
6337
+ task.sourceIssue?.externalIssueId ?? null,
6338
+ task.sourceIssue?.issueNumber ?? null,
6339
+ task.sourceIssue?.url ?? null,
6241
6340
  toJsonNullable(task.mergeDetails),
6242
6341
  task.breakIntoSubtasks ? 1 : 0,
6243
6342
  toJson(task.enabledWorkflowSteps || []),
@@ -6488,6 +6587,7 @@ var init_db_migrate = __esm({
6488
6587
  "../core/src/db-migrate.ts"() {
6489
6588
  "use strict";
6490
6589
  init_db();
6590
+ init_task_priority();
6491
6591
  }
6492
6592
  });
6493
6593
 
@@ -20322,7 +20422,7 @@ var require_luxon = __commonJS({
20322
20422
  }, zone || mergedZone, next];
20323
20423
  }, [{}, null, 1]).slice(0, 2);
20324
20424
  }
20325
- function parse2(s2, ...patterns) {
20425
+ function parse(s2, ...patterns) {
20326
20426
  if (s2 == null) {
20327
20427
  return [null, null];
20328
20428
  }
@@ -20465,26 +20565,26 @@ var require_luxon = __commonJS({
20465
20565
  var extractISOOrdinalDateAndTime = combineExtractors(extractISOOrdinalData, extractISOTime, extractISOOffset, extractIANAZone);
20466
20566
  var extractISOTimeAndOffset = combineExtractors(extractISOTime, extractISOOffset, extractIANAZone);
20467
20567
  function parseISODate(s2) {
20468
- return parse2(s2, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], [isoTimeCombinedRegex, extractISOTimeAndOffset]);
20568
+ return parse(s2, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], [isoTimeCombinedRegex, extractISOTimeAndOffset]);
20469
20569
  }
20470
20570
  function parseRFC2822Date(s2) {
20471
- return parse2(preprocessRFC2822(s2), [rfc2822, extractRFC2822]);
20571
+ return parse(preprocessRFC2822(s2), [rfc2822, extractRFC2822]);
20472
20572
  }
20473
20573
  function parseHTTPDate(s2) {
20474
- return parse2(s2, [rfc1123, extractRFC1123Or850], [rfc850, extractRFC1123Or850], [ascii, extractASCII]);
20574
+ return parse(s2, [rfc1123, extractRFC1123Or850], [rfc850, extractRFC1123Or850], [ascii, extractASCII]);
20475
20575
  }
20476
20576
  function parseISODuration(s2) {
20477
- return parse2(s2, [isoDuration, extractISODuration]);
20577
+ return parse(s2, [isoDuration, extractISODuration]);
20478
20578
  }
20479
20579
  var extractISOTimeOnly = combineExtractors(extractISOTime);
20480
20580
  function parseISOTimeOnly(s2) {
20481
- return parse2(s2, [isoTimeOnly, extractISOTimeOnly]);
20581
+ return parse(s2, [isoTimeOnly, extractISOTimeOnly]);
20482
20582
  }
20483
20583
  var sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex);
20484
20584
  var sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex);
20485
20585
  var extractISOTimeOffsetAndIANAZone = combineExtractors(extractISOTime, extractISOOffset, extractIANAZone);
20486
20586
  function parseSQL(s2) {
20487
- return parse2(s2, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]);
20587
+ return parse(s2, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]);
20488
20588
  }
20489
20589
  var INVALID$2 = "Invalid Duration";
20490
20590
  var lowOrderMatrix = {
@@ -26807,13 +26907,14 @@ __export(automation_store_exports, {
26807
26907
  import { EventEmitter as EventEmitter10 } from "node:events";
26808
26908
  import { join as join12 } from "node:path";
26809
26909
  import { randomUUID as randomUUID5 } from "node:crypto";
26810
- var import_cron_parser, AutomationStore;
26910
+ var import_cron_parser, CRON_TIMEZONE, AutomationStore;
26811
26911
  var init_automation_store = __esm({
26812
26912
  "../core/src/automation-store.ts"() {
26813
26913
  "use strict";
26814
26914
  import_cron_parser = __toESM(require_dist2(), 1);
26815
26915
  init_automation();
26816
26916
  init_db();
26917
+ CRON_TIMEZONE = "UTC";
26817
26918
  AutomationStore = class _AutomationStore extends EventEmitter10 {
26818
26919
  constructor(rootDir) {
26819
26920
  super();
@@ -26931,7 +27032,8 @@ var init_automation_store = __esm({
26931
27032
  */
26932
27033
  computeNextRun(cronExpression, fromDate) {
26933
27034
  const interval = import_cron_parser.CronExpressionParser.parse(cronExpression, {
26934
- currentDate: fromDate ?? /* @__PURE__ */ new Date()
27035
+ currentDate: fromDate ?? /* @__PURE__ */ new Date(),
27036
+ tz: CRON_TIMEZONE
26935
27037
  });
26936
27038
  const next = interval.next();
26937
27039
  return next.toISOString() ?? new Date(next.getTime()).toISOString();
@@ -28551,8 +28653,8 @@ This project has OpenClaw-style memory files:
28551
28653
  - \`.fusion/memory/YYYY-MM-DD.md\` \u2014 append-only daily notes for running context
28552
28654
 
28553
28655
  **Before writing the specification:**
28554
- 1. Use \`memory_search\` first for task-relevant context
28555
- 2. Use \`memory_get\` only for specific memory files/line ranges returned by search
28656
+ 1. Use \`fn_memory_search\` first for task-relevant context
28657
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28556
28658
  3. Incorporate relevant learnings into your specification \u2014 reference actual patterns, constraints, and conventions documented there
28557
28659
 
28558
28660
  Do not read all memory directly by default. If memory is irrelevant, skip it.
@@ -28564,8 +28666,8 @@ Do not read all memory directly by default. If memory is irrelevant, skip it.
28564
28666
  This project has a memory system that stores durable project learnings.
28565
28667
 
28566
28668
  **Before writing the specification:**
28567
- 1. Use \`memory_search\` first for task-relevant context
28568
- 2. Use \`memory_get\` only for specific memory files/line ranges returned by search
28669
+ 1. Use \`fn_memory_search\` first for task-relevant context
28670
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28569
28671
  3. Incorporate useful learnings into your specification
28570
28672
 
28571
28673
  **If the memory contains useful context for this task, reference it in the specification.**
@@ -28597,12 +28699,12 @@ This project has OpenClaw-style memory files:
28597
28699
  - \`.fusion/memory/YYYY-MM-DD.md\` \u2014 append-only daily notes for running observations and open loops
28598
28700
 
28599
28701
  **At the start of execution:**
28600
- 1. Use \`memory_search\` first for task-relevant context
28601
- 2. Use \`memory_get\` only for specific memory files/line ranges returned by search
28702
+ 1. Use \`fn_memory_search\` first for task-relevant context
28703
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28602
28704
  3. Apply relevant learnings to your implementation \u2014 follow documented patterns and avoid known pitfalls
28603
28705
  4. Do not load all memory directly by default. Skip memory reads when memory is irrelevant or context is tight.
28604
28706
 
28605
- **At the end of execution (before calling \`task_done()\`):**
28707
+ **At the end of execution (before calling \`fn_task_done()\`):**
28606
28708
  1. Review what you learned during this task that would genuinely benefit future runs
28607
28709
  2. Write durable decisions, conventions, and pitfalls to \`.fusion/memory/MEMORY.md\`
28608
28710
  3. Write running observations, unresolved context, and open loops to today's \`.fusion/memory/YYYY-MM-DD.md\`
@@ -28631,11 +28733,11 @@ This project has OpenClaw-style memory files:
28631
28733
  This project has a memory system that stores durable project learnings accumulated from past task runs.
28632
28734
 
28633
28735
  **At the start of execution:**
28634
- 1. Use \`memory_search\` first for task-relevant context
28635
- 2. Use \`memory_get\` only for specific memory files/line ranges returned by search
28736
+ 1. Use \`fn_memory_search\` first for task-relevant context
28737
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28636
28738
  3. Apply useful learnings to your implementation
28637
28739
 
28638
- **At the end of execution (before calling \`task_done()\`):**
28740
+ **At the end of execution (before calling \`fn_task_done()\`):**
28639
28741
  1. Review what you learned during this task that would genuinely benefit future runs
28640
28742
  2. **If nothing durable was learned, skip the memory update entirely** \u2014 do not append trivial or task-specific notes
28641
28743
  3. Only write when you have genuinely durable, reusable insights such as:
@@ -28663,8 +28765,8 @@ function buildReviewerMemoryInstructions(rootDir, settings) {
28663
28765
  This project has a memory system that stores durable project learnings.
28664
28766
 
28665
28767
  **During review:**
28666
- 1. Use \`memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict
28667
- 2. Use \`memory_get\` only for specific memory files/line ranges returned by search
28768
+ 1. Use \`fn_memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict
28769
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28668
28770
  3. Treat documented durable conventions and pitfalls as review evidence when deciding APPROVE, REVISE, or RETHINK
28669
28771
  4. Do not update memory during review; reviewer memory access is read-only
28670
28772
  5. Skip memory reads when they are not relevant to the reviewed plan or code
@@ -28786,23 +28888,29 @@ var init_run_command = __esm({
28786
28888
  });
28787
28889
 
28788
28890
  // ../core/src/logger.ts
28891
+ function withSeverityMarker(level, payload) {
28892
+ return `${LOG_LEVEL_MARKER_PREFIX}${level}${LOG_LEVEL_MARKER_SUFFIX}${payload}`;
28893
+ }
28789
28894
  function createLogger(prefix) {
28790
28895
  const tag = `[${prefix}]`;
28791
28896
  return {
28792
28897
  log(message, ...args) {
28793
- console.error(`${tag} ${message}`, ...args);
28898
+ console.error(withSeverityMarker("info", `${tag} ${message}`), ...args);
28794
28899
  },
28795
28900
  warn(message, ...args) {
28796
- console.warn(`${tag} ${message}`, ...args);
28901
+ console.warn(withSeverityMarker("warn", `${tag} ${message}`), ...args);
28797
28902
  },
28798
28903
  error(message, ...args) {
28799
- console.error(`${tag} ${message}`, ...args);
28904
+ console.error(withSeverityMarker("error", `${tag} ${message}`), ...args);
28800
28905
  }
28801
28906
  };
28802
28907
  }
28908
+ var LOG_LEVEL_MARKER_PREFIX, LOG_LEVEL_MARKER_SUFFIX;
28803
28909
  var init_logger = __esm({
28804
28910
  "../core/src/logger.ts"() {
28805
28911
  "use strict";
28912
+ LOG_LEVEL_MARKER_PREFIX = "\0fnlvl=";
28913
+ LOG_LEVEL_MARKER_SUFFIX = "\0";
28806
28914
  }
28807
28915
  });
28808
28916
 
@@ -28854,6 +28962,7 @@ var init_store = __esm({
28854
28962
  "../core/src/store.ts"() {
28855
28963
  "use strict";
28856
28964
  init_types();
28965
+ init_task_priority();
28857
28966
  init_global_settings();
28858
28967
  init_db();
28859
28968
  init_archive_db();
@@ -29043,6 +29152,7 @@ var init_store = __esm({
29043
29152
  id: row.id,
29044
29153
  title: row.title || void 0,
29045
29154
  description: row.description,
29155
+ priority: normalizeTaskPriority(row.priority),
29046
29156
  column: row.column,
29047
29157
  status: row.status || void 0,
29048
29158
  size: row.size || void 0,
@@ -29078,6 +29188,19 @@ var init_store = __esm({
29078
29188
  dependencies: fromJson(row.dependencies) || [],
29079
29189
  steps: fromJson(row.steps) || [],
29080
29190
  log: fromJson(row.log) || [],
29191
+ tokenUsage: (() => {
29192
+ if (row.tokenUsageInputTokens === null || row.tokenUsageOutputTokens === null || row.tokenUsageCachedTokens === null || row.tokenUsageTotalTokens === null || row.tokenUsageFirstUsedAt === null || row.tokenUsageLastUsedAt === null) {
29193
+ return void 0;
29194
+ }
29195
+ return {
29196
+ inputTokens: row.tokenUsageInputTokens,
29197
+ outputTokens: row.tokenUsageOutputTokens,
29198
+ cachedTokens: row.tokenUsageCachedTokens,
29199
+ totalTokens: row.tokenUsageTotalTokens,
29200
+ firstUsedAt: row.tokenUsageFirstUsedAt,
29201
+ lastUsedAt: row.tokenUsageLastUsedAt
29202
+ };
29203
+ })(),
29081
29204
  attachments: (() => {
29082
29205
  const a = fromJson(row.attachments);
29083
29206
  return a && a.length > 0 ? a : void 0;
@@ -29102,6 +29225,18 @@ var init_store = __esm({
29102
29225
  })(),
29103
29226
  prInfo: fromJson(row.prInfo),
29104
29227
  issueInfo: fromJson(row.issueInfo),
29228
+ sourceIssue: (() => {
29229
+ if (row.sourceIssueProvider === null || row.sourceIssueRepository === null || row.sourceIssueExternalIssueId === null || row.sourceIssueNumber === null) {
29230
+ return void 0;
29231
+ }
29232
+ return {
29233
+ provider: row.sourceIssueProvider,
29234
+ repository: row.sourceIssueRepository,
29235
+ externalIssueId: row.sourceIssueExternalIssueId,
29236
+ issueNumber: row.sourceIssueNumber,
29237
+ url: row.sourceIssueUrl ?? void 0
29238
+ };
29239
+ })(),
29105
29240
  mergeDetails: fromJson(row.mergeDetails),
29106
29241
  breakIntoSubtasks: row.breakIntoSubtasks ? true : void 0,
29107
29242
  enabledWorkflowSteps: (() => {
@@ -29125,6 +29260,7 @@ var init_store = __esm({
29125
29260
  id: entry.id,
29126
29261
  title: entry.title,
29127
29262
  description: entry.description,
29263
+ priority: normalizeTaskPriority(entry.priority),
29128
29264
  column: "archived",
29129
29265
  dependencies: entry.dependencies ?? [],
29130
29266
  steps: entry.steps ?? [],
@@ -29133,6 +29269,7 @@ var init_store = __esm({
29133
29269
  reviewLevel: entry.reviewLevel,
29134
29270
  prInfo: slim ? void 0 : entry.prInfo,
29135
29271
  issueInfo: slim ? void 0 : entry.issueInfo,
29272
+ sourceIssue: slim ? void 0 : entry.sourceIssue,
29136
29273
  attachments: slim ? void 0 : entry.attachments,
29137
29274
  comments: entry.comments,
29138
29275
  log: slim ? [] : entry.log ?? [],
@@ -29221,6 +29358,7 @@ ${recentText}` : void 0
29221
29358
  id: task.id,
29222
29359
  title: task.title,
29223
29360
  description: task.description,
29361
+ priority: normalizeTaskPriority(task.priority),
29224
29362
  column: "archived",
29225
29363
  dependencies: task.dependencies,
29226
29364
  steps: task.steps,
@@ -29229,6 +29367,7 @@ ${recentText}` : void 0
29229
29367
  reviewLevel: task.reviewLevel,
29230
29368
  prInfo: task.prInfo,
29231
29369
  issueInfo: task.issueInfo,
29370
+ sourceIssue: task.sourceIssue,
29232
29371
  attachments: task.attachments,
29233
29372
  comments: task.comments,
29234
29373
  prompt,
@@ -29297,6 +29436,7 @@ ${recentText}` : void 0
29297
29436
  "id",
29298
29437
  "title",
29299
29438
  "description",
29439
+ "priority",
29300
29440
  '"column"',
29301
29441
  "status",
29302
29442
  "size",
@@ -29326,6 +29466,12 @@ ${recentText}` : void 0
29326
29466
  "summary",
29327
29467
  "thinkingLevel",
29328
29468
  "executionMode",
29469
+ "tokenUsageInputTokens",
29470
+ "tokenUsageOutputTokens",
29471
+ "tokenUsageCachedTokens",
29472
+ "tokenUsageTotalTokens",
29473
+ "tokenUsageFirstUsedAt",
29474
+ "tokenUsageLastUsedAt",
29329
29475
  "createdAt",
29330
29476
  "updatedAt",
29331
29477
  "columnMovedAt",
@@ -29337,6 +29483,11 @@ ${recentText}` : void 0
29337
29483
  "attachments",
29338
29484
  "prInfo",
29339
29485
  "issueInfo",
29486
+ "sourceIssueProvider",
29487
+ "sourceIssueRepository",
29488
+ "sourceIssueExternalIssueId",
29489
+ "sourceIssueNumber",
29490
+ "sourceIssueUrl",
29340
29491
  "mergeDetails",
29341
29492
  "breakIntoSubtasks",
29342
29493
  "enabledWorkflowSteps",
@@ -29354,6 +29505,7 @@ ${recentText}` : void 0
29354
29505
  "id",
29355
29506
  "title",
29356
29507
  "description",
29508
+ "priority",
29357
29509
  '"column"',
29358
29510
  "status",
29359
29511
  "size",
@@ -29383,6 +29535,12 @@ ${recentText}` : void 0
29383
29535
  "summary",
29384
29536
  "thinkingLevel",
29385
29537
  "executionMode",
29538
+ "tokenUsageInputTokens",
29539
+ "tokenUsageOutputTokens",
29540
+ "tokenUsageCachedTokens",
29541
+ "tokenUsageTotalTokens",
29542
+ "tokenUsageFirstUsedAt",
29543
+ "tokenUsageLastUsedAt",
29386
29544
  "createdAt",
29387
29545
  "updatedAt",
29388
29546
  "columnMovedAt",
@@ -29394,6 +29552,11 @@ ${recentText}` : void 0
29394
29552
  "workflowStepResults",
29395
29553
  "prInfo",
29396
29554
  "issueInfo",
29555
+ "sourceIssueProvider",
29556
+ "sourceIssueRepository",
29557
+ "sourceIssueExternalIssueId",
29558
+ "sourceIssueNumber",
29559
+ "sourceIssueUrl",
29397
29560
  "mergeDetails",
29398
29561
  "breakIntoSubtasks",
29399
29562
  "enabledWorkflowSteps",
@@ -29431,21 +29594,23 @@ ${recentText}` : void 0
29431
29594
  upsertTask(task) {
29432
29595
  this.db.prepare(`
29433
29596
  INSERT INTO tasks (
29434
- id, title, description, "column", status, size, reviewLevel, currentStep,
29597
+ id, title, description, priority, "column", status, size, reviewLevel, currentStep,
29435
29598
  worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
29436
29599
  modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
29437
29600
  workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, nextRecoveryAt, error,
29438
- summary, thinkingLevel, executionMode, createdAt, updatedAt, columnMovedAt,
29601
+ summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
29602
+ tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
29439
29603
  dependencies, steps, log, attachments, steeringComments,
29440
- comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
29441
- breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
29604
+ comments, workflowStepResults, prInfo, issueInfo,
29605
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
29606
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
29442
29607
  ) VALUES (
29443
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
29444
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
29608
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
29445
29609
  )
29446
29610
  ON CONFLICT(id) DO UPDATE SET
29447
29611
  title = excluded.title,
29448
29612
  description = excluded.description,
29613
+ priority = excluded.priority,
29449
29614
  "column" = excluded."column",
29450
29615
  status = excluded.status,
29451
29616
  size = excluded.size,
@@ -29475,6 +29640,12 @@ ${recentText}` : void 0
29475
29640
  summary = excluded.summary,
29476
29641
  thinkingLevel = excluded.thinkingLevel,
29477
29642
  executionMode = excluded.executionMode,
29643
+ tokenUsageInputTokens = excluded.tokenUsageInputTokens,
29644
+ tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
29645
+ tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
29646
+ tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
29647
+ tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
29648
+ tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
29478
29649
  createdAt = excluded.createdAt,
29479
29650
  updatedAt = excluded.updatedAt,
29480
29651
  columnMovedAt = excluded.columnMovedAt,
@@ -29487,6 +29658,11 @@ ${recentText}` : void 0
29487
29658
  workflowStepResults = excluded.workflowStepResults,
29488
29659
  prInfo = excluded.prInfo,
29489
29660
  issueInfo = excluded.issueInfo,
29661
+ sourceIssueProvider = excluded.sourceIssueProvider,
29662
+ sourceIssueRepository = excluded.sourceIssueRepository,
29663
+ sourceIssueExternalIssueId = excluded.sourceIssueExternalIssueId,
29664
+ sourceIssueNumber = excluded.sourceIssueNumber,
29665
+ sourceIssueUrl = excluded.sourceIssueUrl,
29490
29666
  mergeDetails = excluded.mergeDetails,
29491
29667
  breakIntoSubtasks = excluded.breakIntoSubtasks,
29492
29668
  enabledWorkflowSteps = excluded.enabledWorkflowSteps,
@@ -29501,6 +29677,7 @@ ${recentText}` : void 0
29501
29677
  task.id,
29502
29678
  task.title ?? null,
29503
29679
  task.description,
29680
+ normalizeTaskPriority(task.priority),
29504
29681
  task.column,
29505
29682
  task.status ?? null,
29506
29683
  task.size ?? null,
@@ -29530,6 +29707,12 @@ ${recentText}` : void 0
29530
29707
  task.summary ?? null,
29531
29708
  task.thinkingLevel ?? null,
29532
29709
  task.executionMode ?? null,
29710
+ task.tokenUsage?.inputTokens ?? null,
29711
+ task.tokenUsage?.outputTokens ?? null,
29712
+ task.tokenUsage?.cachedTokens ?? null,
29713
+ task.tokenUsage?.totalTokens ?? null,
29714
+ task.tokenUsage?.firstUsedAt ?? null,
29715
+ task.tokenUsage?.lastUsedAt ?? null,
29533
29716
  task.createdAt,
29534
29717
  task.updatedAt,
29535
29718
  task.columnMovedAt ?? null,
@@ -29542,6 +29725,11 @@ ${recentText}` : void 0
29542
29725
  toJson(task.workflowStepResults || []),
29543
29726
  toJsonNullable(task.prInfo),
29544
29727
  toJsonNullable(task.issueInfo),
29728
+ task.sourceIssue?.provider ?? null,
29729
+ task.sourceIssue?.repository ?? null,
29730
+ task.sourceIssue?.externalIssueId ?? null,
29731
+ task.sourceIssue?.issueNumber ?? null,
29732
+ task.sourceIssue?.url ?? null,
29545
29733
  toJsonNullable(task.mergeDetails),
29546
29734
  task.breakIntoSubtasks ? 1 : 0,
29547
29735
  toJson(task.enabledWorkflowSteps || []),
@@ -29746,6 +29934,7 @@ ${recentText}` : void 0
29746
29934
  if (!Array.isArray(fileTask.log)) fileTask.log = [];
29747
29935
  if (!Array.isArray(fileTask.dependencies)) fileTask.dependencies = [];
29748
29936
  if (!Array.isArray(fileTask.steps)) fileTask.steps = [];
29937
+ fileTask.priority = normalizeTaskPriority(fileTask.priority);
29749
29938
  return fileTask;
29750
29939
  } catch (err) {
29751
29940
  throw new Error(
@@ -30280,6 +30469,9 @@ ${recentText}` : void 0
30280
30469
  id,
30281
30470
  title,
30282
30471
  description: input.description,
30472
+ priority: normalizeTaskPriority(input.priority),
30473
+ tokenUsage: input.tokenUsage,
30474
+ sourceIssue: input.sourceIssue,
30283
30475
  column: input.column || "triage",
30284
30476
  dependencies: input.dependencies || [],
30285
30477
  breakIntoSubtasks: input.breakIntoSubtasks === true ? true : void 0,
@@ -30334,6 +30526,7 @@ ${task.description}
30334
30526
  description: `${sourceTask.description}
30335
30527
 
30336
30528
  (Duplicated from ${id})`,
30529
+ priority: normalizeTaskPriority(sourceTask.priority),
30337
30530
  column: "triage",
30338
30531
  modelPresetId: sourceTask.modelPresetId,
30339
30532
  dependencies: [],
@@ -30392,6 +30585,7 @@ ${task.description}
30392
30585
  description: `${feedback.trim()}
30393
30586
 
30394
30587
  Refines: ${id}`,
30588
+ priority: normalizeTaskPriority(sourceTask.priority),
30395
30589
  column: "triage",
30396
30590
  dependencies: [id],
30397
30591
  // Refinement depends on the original being complete
@@ -30716,6 +30910,11 @@ ${newTask.description}
30716
30910
  }
30717
30911
  if (updates.title !== void 0) task.title = updates.title;
30718
30912
  if (updates.description !== void 0) task.description = updates.description;
30913
+ if (updates.priority === null) {
30914
+ task.priority = normalizeTaskPriority(void 0);
30915
+ } else if (updates.priority !== void 0) {
30916
+ task.priority = normalizeTaskPriority(updates.priority);
30917
+ }
30719
30918
  if (updates.worktree === null) {
30720
30919
  task.worktree = void 0;
30721
30920
  } else if (updates.worktree !== void 0) {
@@ -30883,6 +31082,16 @@ ${newTask.description}
30883
31082
  } else if (updates.mergeDetails !== void 0) {
30884
31083
  task.mergeDetails = updates.mergeDetails;
30885
31084
  }
31085
+ if (updates.sourceIssue === null) {
31086
+ task.sourceIssue = void 0;
31087
+ } else if (updates.sourceIssue !== void 0) {
31088
+ task.sourceIssue = updates.sourceIssue;
31089
+ }
31090
+ if (updates.tokenUsage === null) {
31091
+ task.tokenUsage = void 0;
31092
+ } else if (updates.tokenUsage !== void 0) {
31093
+ task.tokenUsage = updates.tokenUsage;
31094
+ }
30886
31095
  if (updates.modifiedFiles === null) {
30887
31096
  task.modifiedFiles = void 0;
30888
31097
  } else if (updates.modifiedFiles !== void 0) {
@@ -31283,14 +31492,14 @@ ${task.description}
31283
31492
  }
31284
31493
  return paths;
31285
31494
  }
31286
- async deleteTask(id) {
31495
+ async deleteTask(id, options) {
31287
31496
  return this.withTaskLock(id, async () => {
31288
31497
  const task = this.readTaskFromDb(id);
31289
31498
  if (!task) {
31290
31499
  throw new Error(`Task ${id} not found`);
31291
31500
  }
31292
31501
  const dependentIds = this.findLiveDependents(id);
31293
- if (dependentIds.length > 0) {
31502
+ if (dependentIds.length > 0 && !options?.removeDependencyReferences) {
31294
31503
  throw new TaskHasDependentsError(id, dependentIds);
31295
31504
  }
31296
31505
  const cleanedBranches = await this.cleanupBranchForTask(task);
@@ -31301,18 +31510,50 @@ ${task.description}
31301
31510
  action: `Cleaned up branch: ${cleanedBranches.join(", ")}`
31302
31511
  });
31303
31512
  }
31304
- this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
31305
- this.db.bumpLastModified();
31513
+ const rewrittenDependents = this.rewriteDependentsAndDeleteTask(id, dependentIds);
31306
31514
  if (this.isWatching) this.taskCache.delete(id);
31307
31515
  const dir = this.taskDir(id);
31308
31516
  if (existsSync12(dir)) {
31309
- const { rm: rm2 } = await import("node:fs/promises");
31310
- await rm2(dir, { recursive: true });
31517
+ const { rm: rm3 } = await import("node:fs/promises");
31518
+ await rm3(dir, { recursive: true });
31519
+ }
31520
+ for (const dependentTask of rewrittenDependents) {
31521
+ this.emit("task:updated", dependentTask);
31311
31522
  }
31312
31523
  this.emit("task:deleted", task);
31313
31524
  return task;
31314
31525
  });
31315
31526
  }
31527
+ rewriteDependentsAndDeleteTask(taskId, dependentIds) {
31528
+ const rewrittenDependents = [];
31529
+ this.db.transaction(() => {
31530
+ for (const dependentId of dependentIds) {
31531
+ const dependentTask = this.readTaskFromDb(dependentId);
31532
+ if (!dependentTask) continue;
31533
+ const nextDependencies = dependentTask.dependencies.filter((dependencyId) => dependencyId !== taskId);
31534
+ if (nextDependencies.length === dependentTask.dependencies.length) {
31535
+ continue;
31536
+ }
31537
+ const updatedDependent = {
31538
+ ...dependentTask,
31539
+ dependencies: nextDependencies,
31540
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
31541
+ };
31542
+ this.db.prepare("UPDATE tasks SET dependencies = ?, updatedAt = ? WHERE id = ?").run(
31543
+ toJson(updatedDependent.dependencies),
31544
+ updatedDependent.updatedAt,
31545
+ updatedDependent.id
31546
+ );
31547
+ if (this.isWatching) {
31548
+ this.taskCache.set(updatedDependent.id, updatedDependent);
31549
+ }
31550
+ rewrittenDependents.push(updatedDependent);
31551
+ }
31552
+ this.db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
31553
+ this.db.bumpLastModified();
31554
+ });
31555
+ return rewrittenDependents;
31556
+ }
31316
31557
  /**
31317
31558
  * Clean up the git branch associated with a task.
31318
31559
  *
@@ -31609,8 +31850,8 @@ ${task.description}
31609
31850
  this.archiveDb.upsert(entry);
31610
31851
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
31611
31852
  this.db.bumpLastModified();
31612
- const { rm: rm2 } = await import("node:fs/promises");
31613
- await rm2(dir, { recursive: true, force: true });
31853
+ const { rm: rm3 } = await import("node:fs/promises");
31854
+ await rm3(dir, { recursive: true, force: true });
31614
31855
  if (this.isWatching) {
31615
31856
  this.taskCache.delete(id);
31616
31857
  }
@@ -32088,24 +32329,64 @@ ${task.description}
32088
32329
  this.emit("task:updated", task2);
32089
32330
  return task2;
32090
32331
  });
32332
+ const commentContextBase = {
32333
+ taskId: id,
32334
+ author,
32335
+ commentLength: text.length,
32336
+ column: task.column,
32337
+ priorStatus: task.status ?? null
32338
+ };
32339
+ if (runContext) {
32340
+ commentContextBase.runId = runContext.runId;
32341
+ commentContextBase.agentId = runContext.agentId;
32342
+ if (runContext.source) {
32343
+ commentContextBase.runSource = runContext.source;
32344
+ }
32345
+ }
32091
32346
  if (task.column === "done" && author === "user" && !options?.skipRefinement) {
32092
32347
  try {
32093
32348
  await this.refineTask(id, text);
32094
- } catch {
32349
+ } catch (err) {
32350
+ storeLog.warn("Best-effort post-comment auto-refinement failed", {
32351
+ ...commentContextBase,
32352
+ phase: "addComment:auto-refinement",
32353
+ error: err instanceof Error ? err.message : String(err)
32354
+ });
32095
32355
  }
32096
32356
  }
32097
32357
  if (task.column === "triage" && task.status === "awaiting-approval" && author === "user") {
32358
+ let invalidatedStatus = false;
32098
32359
  try {
32099
32360
  await this.updateTask(id, {
32100
32361
  status: "needs-respecify"
32101
32362
  });
32102
- await this.logEntry(
32103
- id,
32104
- `User comment invalidated spec approval \u2014 task needs re-specification`,
32105
- void 0,
32106
- runContext
32107
- );
32108
- } catch {
32363
+ invalidatedStatus = true;
32364
+ } catch (err) {
32365
+ storeLog.warn("Best-effort post-comment awaiting-approval invalidation failed", {
32366
+ ...commentContextBase,
32367
+ phase: "addComment:awaiting-approval-invalidation",
32368
+ stage: "status-update",
32369
+ nextStatus: "needs-respecify",
32370
+ error: err instanceof Error ? err.message : String(err)
32371
+ });
32372
+ }
32373
+ if (invalidatedStatus) {
32374
+ try {
32375
+ await this.logEntry(
32376
+ id,
32377
+ `User comment invalidated spec approval \u2014 task needs re-specification`,
32378
+ void 0,
32379
+ runContext
32380
+ );
32381
+ } catch (err) {
32382
+ storeLog.warn("Best-effort post-comment awaiting-approval invalidation failed", {
32383
+ ...commentContextBase,
32384
+ phase: "addComment:awaiting-approval-invalidation",
32385
+ stage: "post-invalidation-log-entry",
32386
+ nextStatus: "needs-respecify",
32387
+ error: err instanceof Error ? err.message : String(err)
32388
+ });
32389
+ }
32109
32390
  }
32110
32391
  }
32111
32392
  return task;
@@ -32381,7 +32662,7 @@ ${task.description}
32381
32662
  const rows2 = this.db.prepare(`
32382
32663
  SELECT * FROM agentLogEntries
32383
32664
  WHERE taskId = ?
32384
- ORDER BY timestamp DESC
32665
+ ORDER BY timestamp DESC, id DESC
32385
32666
  LIMIT ?
32386
32667
  `).all(taskId, readCount);
32387
32668
  const entries2 = rows2.map((row) => this.mapAgentLogRow(row)).reverse();
@@ -32393,7 +32674,7 @@ ${task.description}
32393
32674
  const rows = this.db.prepare(`
32394
32675
  SELECT * FROM agentLogEntries
32395
32676
  WHERE taskId = ?
32396
- ORDER BY timestamp ASC
32677
+ ORDER BY timestamp ASC, id ASC
32397
32678
  `).all(taskId);
32398
32679
  const entries = rows.map((row) => this.mapAgentLogRow(row));
32399
32680
  if (offset > 0) {
@@ -32426,7 +32707,7 @@ ${task.description}
32426
32707
  const rows = this.db.prepare(`
32427
32708
  SELECT * FROM agentLogEntries
32428
32709
  WHERE taskId = ? AND timestamp >= ? AND timestamp <= ?
32429
- ORDER BY timestamp ASC
32710
+ ORDER BY timestamp ASC, id ASC
32430
32711
  `).all(taskId, startIso, end);
32431
32712
  return rows.map((row) => this.mapAgentLogRow(row));
32432
32713
  }
@@ -32522,14 +32803,14 @@ ${task.description}
32522
32803
  if (rows.length === 0) {
32523
32804
  return;
32524
32805
  }
32525
- const { rm: rm2 } = await import("node:fs/promises");
32806
+ const { rm: rm3 } = await import("node:fs/promises");
32526
32807
  for (const row of rows) {
32527
32808
  const task = this.rowToTask(row);
32528
32809
  const archivedAt = task.columnMovedAt ?? task.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
32529
32810
  const entry = await this.taskToArchiveEntry(task, archivedAt);
32530
32811
  this.archiveDb.upsert(entry);
32531
32812
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
32532
- await rm2(this.taskDir(task.id), { recursive: true, force: true });
32813
+ await rm3(this.taskDir(task.id), { recursive: true, force: true });
32533
32814
  if (this.isWatching) {
32534
32815
  this.taskCache.delete(task.id);
32535
32816
  }
@@ -32552,8 +32833,8 @@ ${task.description}
32552
32833
  this.archiveDb.upsert(entry);
32553
32834
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
32554
32835
  this.db.bumpLastModified();
32555
- const { rm: rm2 } = await import("node:fs/promises");
32556
- await rm2(dir, { recursive: true, force: true });
32836
+ const { rm: rm3 } = await import("node:fs/promises");
32837
+ await rm3(dir, { recursive: true, force: true });
32557
32838
  if (this.isWatching) {
32558
32839
  this.taskCache.delete(task.id);
32559
32840
  }
@@ -32576,6 +32857,7 @@ ${task.description}
32576
32857
  id: entry.id,
32577
32858
  title: entry.title,
32578
32859
  description: entry.description,
32860
+ priority: normalizeTaskPriority(entry.priority),
32579
32861
  column: "archived",
32580
32862
  // Will be changed to "done" by unarchiveTask
32581
32863
  dependencies: entry.dependencies,
@@ -32585,6 +32867,7 @@ ${task.description}
32585
32867
  reviewLevel: entry.reviewLevel,
32586
32868
  prInfo: entry.prInfo,
32587
32869
  issueInfo: entry.issueInfo,
32870
+ sourceIssue: entry.sourceIssue,
32588
32871
  attachments: entry.attachments,
32589
32872
  log: [...entry.log, { timestamp: (/* @__PURE__ */ new Date()).toISOString(), action: "Task restored from archive" }],
32590
32873
  comments: entry.comments,
@@ -33194,6 +33477,28 @@ var init_daemon_token = __esm({
33194
33477
  const settings = await this.settingsStore.getSettings();
33195
33478
  return settings.daemonToken;
33196
33479
  }
33480
+ /**
33481
+ * Retrieve the existing daemon token or create/persist one if missing.
33482
+ *
33483
+ * Safe for concurrent callers: if another process writes the token between
33484
+ * the initial read and generateToken(), this method re-reads and returns the
33485
+ * persisted token instead of failing.
33486
+ */
33487
+ async getOrCreateToken() {
33488
+ const existing = await this.getToken();
33489
+ if (existing) {
33490
+ return existing;
33491
+ }
33492
+ try {
33493
+ return await this.generateToken();
33494
+ } catch (error) {
33495
+ const afterRace = await this.getToken();
33496
+ if (afterRace) {
33497
+ return afterRace;
33498
+ }
33499
+ throw error;
33500
+ }
33501
+ }
33197
33502
  /**
33198
33503
  * Validate that a provided token matches the stored token.
33199
33504
  *
@@ -33637,13 +33942,14 @@ __export(routine_store_exports, {
33637
33942
  });
33638
33943
  import { EventEmitter as EventEmitter12 } from "node:events";
33639
33944
  import { randomUUID as randomUUID7 } from "node:crypto";
33640
- var import_cron_parser2, RoutineStore;
33945
+ var import_cron_parser2, CRON_TIMEZONE2, RoutineStore;
33641
33946
  var init_routine_store = __esm({
33642
33947
  "../core/src/routine-store.ts"() {
33643
33948
  "use strict";
33644
33949
  import_cron_parser2 = __toESM(require_dist2(), 1);
33645
33950
  init_db();
33646
33951
  init_routine();
33952
+ CRON_TIMEZONE2 = "UTC";
33647
33953
  RoutineStore = class _RoutineStore extends EventEmitter12 {
33648
33954
  constructor(rootDir) {
33649
33955
  super();
@@ -33806,7 +34112,8 @@ var init_routine_store = __esm({
33806
34112
  */
33807
34113
  computeNextRun(cronExpression, fromDate) {
33808
34114
  const interval = import_cron_parser2.CronExpressionParser.parse(cronExpression, {
33809
- currentDate: fromDate ?? /* @__PURE__ */ new Date()
34115
+ currentDate: fromDate ?? /* @__PURE__ */ new Date(),
34116
+ tz: CRON_TIMEZONE2
33810
34117
  });
33811
34118
  const next = interval.next();
33812
34119
  return new Date(next.getTime()).toISOString();
@@ -34048,12 +34355,11 @@ var init_routine_store = __esm({
34048
34355
  });
34049
34356
 
34050
34357
  // ../core/src/plugin-loader.ts
34051
- import { randomUUID as randomUUID8 } from "node:crypto";
34052
- import { copyFile, unlink as unlink4 } from "node:fs/promises";
34053
- import { isAbsolute as isAbsolute5, parse, resolve as resolve7 } from "node:path";
34358
+ import { basename as basename6, dirname as dirname5, extname, isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
34359
+ import { copyFile, rm } from "node:fs/promises";
34054
34360
  import { pathToFileURL } from "node:url";
34055
34361
  import { EventEmitter as EventEmitter13 } from "node:events";
34056
- var MINIMUM_FUSION_VERSION, log, PluginLoader;
34362
+ var MINIMUM_FUSION_VERSION, log, moduleImportVersion, PluginLoader;
34057
34363
  var init_plugin_loader = __esm({
34058
34364
  "../core/src/plugin-loader.ts"() {
34059
34365
  "use strict";
@@ -34061,6 +34367,7 @@ var init_plugin_loader = __esm({
34061
34367
  init_logger();
34062
34368
  MINIMUM_FUSION_VERSION = "0.1.0";
34063
34369
  log = createLogger("plugin-loader");
34370
+ moduleImportVersion = 0;
34064
34371
  PluginLoader = class extends EventEmitter13 {
34065
34372
  constructor(options) {
34066
34373
  super();
@@ -34070,8 +34377,6 @@ var init_plugin_loader = __esm({
34070
34377
  plugins = /* @__PURE__ */ new Map();
34071
34378
  /** Cache of dynamically imported modules */
34072
34379
  loadedModules = /* @__PURE__ */ new Map();
34073
- /** Monotonic nonce to guarantee unique cache-busting import URLs. */
34074
- importNonce = 0;
34075
34380
  getProjectRoot() {
34076
34381
  return this.options.taskStore.getRootDir();
34077
34382
  }
@@ -34201,31 +34506,24 @@ var init_plugin_loader = __esm({
34201
34506
  if (!bypassCache && this.loadedModules.has(path)) {
34202
34507
  return this.loadedModules.get(path);
34203
34508
  }
34204
- let importPath = path;
34205
- let tempPath = null;
34509
+ const moduleUrl = pathToFileURL(path).href;
34510
+ let mod;
34206
34511
  if (bypassCache) {
34207
- const parsed = parse(path);
34208
- tempPath = resolve7(
34209
- parsed.dir,
34210
- `${parsed.name}.fusion-import-${process.pid}-${++this.importNonce}-${randomUUID8()}${parsed.ext || ".js"}`
34211
- );
34212
- await copyFile(path, tempPath);
34213
- importPath = tempPath;
34214
- }
34215
- const fileUrl = pathToFileURL(importPath);
34216
- if (bypassCache) {
34217
- fileUrl.searchParams.set("t", `${Date.now()}-${this.importNonce}`);
34218
- }
34219
- try {
34220
- const mod = await import(fileUrl.href);
34221
- this.loadedModules.set(path, mod);
34222
- return mod;
34223
- } finally {
34224
- if (tempPath) {
34225
- void unlink4(tempPath).catch(() => {
34226
- });
34512
+ moduleImportVersion += 1;
34513
+ const ext = extname(path);
34514
+ const baseName = basename6(path, ext);
34515
+ const reloadedPath = resolve7(dirname5(path), `.${baseName}.reload-${moduleImportVersion}${ext}`);
34516
+ await copyFile(path, reloadedPath);
34517
+ try {
34518
+ mod = await import(pathToFileURL(reloadedPath).href);
34519
+ } finally {
34520
+ await rm(reloadedPath, { force: true }).catch(() => void 0);
34227
34521
  }
34522
+ } else {
34523
+ mod = await import(moduleUrl);
34228
34524
  }
34525
+ this.loadedModules.set(path, mod);
34526
+ return mod;
34229
34527
  }
34230
34528
  /**
34231
34529
  * Invalidate the module cache for a plugin path.
@@ -34614,7 +34912,7 @@ var init_plugin_loader = __esm({
34614
34912
  });
34615
34913
 
34616
34914
  // ../core/src/backup.ts
34617
- import { cp, mkdir as mkdir8, readdir as readdir6, stat as stat3, unlink as unlink5 } from "node:fs/promises";
34915
+ import { cp, mkdir as mkdir8, readdir as readdir6, stat as stat3, unlink as unlink4 } from "node:fs/promises";
34618
34916
  import { existsSync as existsSync14 } from "node:fs";
34619
34917
  import { join as join17 } from "node:path";
34620
34918
  function generateBackupFilename() {
@@ -34867,7 +35165,7 @@ var init_backup = __esm({
34867
35165
  let deletedCount = 0;
34868
35166
  for (const backup of toDelete) {
34869
35167
  try {
34870
- await unlink5(backup.path);
35168
+ await unlink4(backup.path);
34871
35169
  deletedCount++;
34872
35170
  } catch {
34873
35171
  }
@@ -35577,7 +35875,7 @@ var init_mission_types = __esm({
35577
35875
  // ../core/src/memory-insights.ts
35578
35876
  import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir9 } from "node:fs/promises";
35579
35877
  import { existsSync as existsSync15 } from "node:fs";
35580
- import { dirname as dirname5, join as join18 } from "node:path";
35878
+ import { dirname as dirname6, join as join18 } from "node:path";
35581
35879
  async function readWorkingMemory(rootDir) {
35582
35880
  const filePath = join18(rootDir, MEMORY_WORKING_PATH);
35583
35881
  if (!existsSync15(filePath)) {
@@ -35602,7 +35900,7 @@ async function writeInsightsMemory(rootDir, content) {
35602
35900
  }
35603
35901
  async function writeWorkingMemory(rootDir, content) {
35604
35902
  const filePath = join18(rootDir, MEMORY_WORKING_PATH);
35605
- const dir = dirname5(filePath);
35903
+ const dir = dirname6(filePath);
35606
35904
  if (!existsSync15(dir)) {
35607
35905
  await mkdir9(dir, { recursive: true });
35608
35906
  }
@@ -36447,7 +36745,7 @@ var require_ms = __commonJS({
36447
36745
  options = options || {};
36448
36746
  var type = typeof val;
36449
36747
  if (type === "string" && val.length > 0) {
36450
- return parse2(val);
36748
+ return parse(val);
36451
36749
  } else if (type === "number" && isFinite(val)) {
36452
36750
  return options.long ? fmtLong(val) : fmtShort(val);
36453
36751
  }
@@ -36455,7 +36753,7 @@ var require_ms = __commonJS({
36455
36753
  "val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
36456
36754
  );
36457
36755
  };
36458
- function parse2(str) {
36756
+ function parse(str) {
36459
36757
  str = String(str);
36460
36758
  if (str.length > 100) {
36461
36759
  return;
@@ -46128,7 +46426,7 @@ var require_public_api = __commonJS({
46128
46426
  }
46129
46427
  return doc;
46130
46428
  }
46131
- function parse2(src, reviver, options) {
46429
+ function parse(src, reviver, options) {
46132
46430
  let _reviver = void 0;
46133
46431
  if (typeof reviver === "function") {
46134
46432
  _reviver = reviver;
@@ -46169,7 +46467,7 @@ var require_public_api = __commonJS({
46169
46467
  return value.toString(options);
46170
46468
  return new Document.Document(value, _replacer, options).toString(options);
46171
46469
  }
46172
- exports.parse = parse2;
46470
+ exports.parse = parse;
46173
46471
  exports.parseAllDocuments = parseAllDocuments;
46174
46472
  exports.parseDocument = parseDocument;
46175
46473
  exports.stringify = stringify;
@@ -46270,10 +46568,10 @@ function pushAlias(aliases, value) {
46270
46568
  if (pathRef !== normalized && pathRef.length > 0) {
46271
46569
  aliases.add(pathRef);
46272
46570
  }
46273
- const basename8 = extractPathBasename(value);
46274
- if (basename8) {
46275
- aliases.add(basename8);
46276
- const basenameSlug = slugifyAgentReference(basename8);
46571
+ const basename9 = extractPathBasename(value);
46572
+ if (basename9) {
46573
+ aliases.add(basename9);
46574
+ const basenameSlug = slugifyAgentReference(basename9);
46277
46575
  if (basenameSlug.length > 0) {
46278
46576
  aliases.add(basenameSlug);
46279
46577
  }
@@ -46959,7 +47257,7 @@ var init_agent_companies_exporter = __esm({
46959
47257
 
46960
47258
  // ../core/src/chat-store.ts
46961
47259
  import { EventEmitter as EventEmitter14 } from "node:events";
46962
- import { randomUUID as randomUUID9 } from "node:crypto";
47260
+ import { randomUUID as randomUUID8 } from "node:crypto";
46963
47261
  var ChatStore;
46964
47262
  var init_chat_store = __esm({
46965
47263
  "../core/src/chat-store.ts"() {
@@ -47012,7 +47310,7 @@ var init_chat_store = __esm({
47012
47310
  */
47013
47311
  createSession(input) {
47014
47312
  const now = (/* @__PURE__ */ new Date()).toISOString();
47015
- const id = `chat-${randomUUID9().slice(0, 8)}`;
47313
+ const id = `chat-${randomUUID8().slice(0, 8)}`;
47016
47314
  const session = {
47017
47315
  id,
47018
47316
  agentId: input.agentId,
@@ -47080,6 +47378,58 @@ var init_chat_store = __esm({
47080
47378
  `).all(...params);
47081
47379
  return rows.map((row) => this.rowToSession(row));
47082
47380
  }
47381
+ /**
47382
+ * Find the newest active session for a specific quick-chat target.
47383
+ *
47384
+ * Matching semantics:
47385
+ * - model target (`modelProvider` + `modelId`): exact agent+model match
47386
+ * - agent target (no model): prefer model-less sessions, then newest agent session fallback
47387
+ */
47388
+ findLatestActiveSessionForTarget(options) {
47389
+ const normalizedAgentId = options.agentId.trim();
47390
+ if (!normalizedAgentId) {
47391
+ return void 0;
47392
+ }
47393
+ const normalizedProvider = options.modelProvider?.trim();
47394
+ const normalizedModelId = options.modelId?.trim();
47395
+ if (normalizedProvider && !normalizedModelId || !normalizedProvider && normalizedModelId) {
47396
+ throw new Error("modelProvider and modelId must both be provided together, or neither");
47397
+ }
47398
+ const whereClauses = ["status = ?", "agentId = ?"];
47399
+ const baseParams = ["active", normalizedAgentId];
47400
+ if (options.projectId && options.projectId.trim()) {
47401
+ whereClauses.push("projectId = ?");
47402
+ baseParams.push(options.projectId.trim());
47403
+ }
47404
+ const baseWhereSql = whereClauses.join(" AND ");
47405
+ if (normalizedProvider && normalizedModelId) {
47406
+ const row = this.db.prepare(`
47407
+ SELECT * FROM chat_sessions
47408
+ WHERE ${baseWhereSql} AND modelProvider = ? AND modelId = ?
47409
+ ORDER BY updatedAt DESC
47410
+ LIMIT 1
47411
+ `).get(...baseParams, normalizedProvider, normalizedModelId);
47412
+ return row ? this.rowToSession(row) : void 0;
47413
+ }
47414
+ const modelLessRow = this.db.prepare(`
47415
+ SELECT * FROM chat_sessions
47416
+ WHERE ${baseWhereSql}
47417
+ AND COALESCE(TRIM(modelProvider), '') = ''
47418
+ AND COALESCE(TRIM(modelId), '') = ''
47419
+ ORDER BY updatedAt DESC
47420
+ LIMIT 1
47421
+ `).get(...baseParams);
47422
+ if (modelLessRow) {
47423
+ return this.rowToSession(modelLessRow);
47424
+ }
47425
+ const fallbackRow = this.db.prepare(`
47426
+ SELECT * FROM chat_sessions
47427
+ WHERE ${baseWhereSql}
47428
+ ORDER BY updatedAt DESC
47429
+ LIMIT 1
47430
+ `).get(...baseParams);
47431
+ return fallbackRow ? this.rowToSession(fallbackRow) : void 0;
47432
+ }
47083
47433
  /**
47084
47434
  * Update a chat session.
47085
47435
  *
@@ -47158,7 +47508,7 @@ var init_chat_store = __esm({
47158
47508
  throw new Error(`Chat session ${sessionId} not found`);
47159
47509
  }
47160
47510
  const now = (/* @__PURE__ */ new Date()).toISOString();
47161
- const id = `msg-${randomUUID9().slice(0, 8)}`;
47511
+ const id = `msg-${randomUUID8().slice(0, 8)}`;
47162
47512
  const message = {
47163
47513
  id,
47164
47514
  sessionId,
@@ -47312,6 +47662,7 @@ __export(src_exports, {
47312
47662
  DEFAULT_MIN_INTERVAL_MS: () => DEFAULT_MIN_INTERVAL_MS,
47313
47663
  DEFAULT_PROJECT_SETTINGS: () => DEFAULT_PROJECT_SETTINGS,
47314
47664
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
47665
+ DEFAULT_TASK_PRIORITY: () => DEFAULT_TASK_PRIORITY,
47315
47666
  DaemonTokenManager: () => DaemonTokenManager,
47316
47667
  Database: () => Database,
47317
47668
  EXECUTION_MODES: () => EXECUTION_MODES,
@@ -47367,6 +47718,7 @@ __export(src_exports, {
47367
47718
  SLICE_PLAN_STATES: () => SLICE_PLAN_STATES,
47368
47719
  SLICE_STATUSES: () => SLICE_STATUSES,
47369
47720
  SUMMARIZE_SYSTEM_PROMPT: () => SUMMARIZE_SYSTEM_PROMPT,
47721
+ TASK_PRIORITIES: () => TASK_PRIORITIES,
47370
47722
  THEME_MODES: () => THEME_MODES,
47371
47723
  THINKING_LEVELS: () => THINKING_LEVELS,
47372
47724
  TaskStore: () => TaskStore,
@@ -47399,6 +47751,8 @@ __export(src_exports, {
47399
47751
  clearOverrides: () => clearOverrides,
47400
47752
  collectSystemMetrics: () => collectSystemMetrics,
47401
47753
  compactMemoryWithAi: () => compactMemoryWithAi,
47754
+ compareTaskPriority: () => compareTaskPriority,
47755
+ compareTasksByPriorityThenAgeAndId: () => compareTasksByPriorityThenAgeAndId,
47402
47756
  computeAccessState: () => computeAccessState,
47403
47757
  computeInsightFingerprint: () => computeInsightFingerprint,
47404
47758
  convertAgentCompanies: () => convertAgentCompanies,
@@ -47454,6 +47808,7 @@ __export(src_exports, {
47454
47808
  getRateLimitResetTime: () => getRateLimitResetTime,
47455
47809
  getTaskCompletionBlocker: () => getTaskCompletionBlocker,
47456
47810
  getTaskMergeBlocker: () => getTaskMergeBlocker,
47811
+ getTaskPriorityRank: () => getTaskPriorityRank,
47457
47812
  getTemplatesForRole: () => getTemplatesForRole,
47458
47813
  getValidTransitions: () => getValidTransitions,
47459
47814
  hasAgentIdentity: () => hasAgentIdentity,
@@ -47470,6 +47825,7 @@ __export(src_exports, {
47470
47825
  isManualTrigger: () => isManualTrigger,
47471
47826
  isProjectSettingsKey: () => isProjectSettingsKey,
47472
47827
  isQmdAvailable: () => isQmdAvailable,
47828
+ isTaskPriority: () => isTaskPriority,
47473
47829
  isTaskReadyForMerge: () => isTaskReadyForMerge,
47474
47830
  isValidPermission: () => isValidPermission,
47475
47831
  isValidPromptKey: () => isValidPromptKey,
@@ -47493,6 +47849,7 @@ __export(src_exports, {
47493
47849
  normalizePermissions: () => normalizePermissions,
47494
47850
  normalizeRoadmapFeatureOrder: () => normalizeRoadmapFeatureOrder,
47495
47851
  normalizeRoadmapMilestoneOrder: () => normalizeRoadmapMilestoneOrder,
47852
+ normalizeTaskPriority: () => normalizeTaskPriority,
47496
47853
  parseAgentManifest: () => parseAgentManifest,
47497
47854
  parseCompanyArchive: () => parseCompanyArchive,
47498
47855
  parseCompanyDirectory: () => parseCompanyDirectory,
@@ -47545,6 +47902,7 @@ __export(src_exports, {
47545
47902
  shouldSkipBackgroundQmdRefresh: () => shouldSkipBackgroundQmdRefresh,
47546
47903
  shouldTriggerExtraction: () => shouldTriggerExtraction,
47547
47904
  slugify: () => slugify,
47905
+ sortTasksByPriorityThenAgeAndId: () => sortTasksByPriorityThenAgeAndId,
47548
47906
  summarizeTitle: () => summarizeTitle,
47549
47907
  syncAutoSummarizeAutomation: () => syncAutoSummarizeAutomation,
47550
47908
  syncBackupAutomation: () => syncBackupAutomation,
@@ -47602,6 +47960,7 @@ var init_src = __esm({
47602
47960
  init_ai_summarize();
47603
47961
  init_memory_compaction();
47604
47962
  init_roadmap_ordering();
47963
+ init_task_priority();
47605
47964
  init_roadmap_handoff();
47606
47965
  init_mission_types();
47607
47966
  init_mission_store();
@@ -47627,25 +47986,50 @@ var init_src = __esm({
47627
47986
  }
47628
47987
  });
47629
47988
 
47630
- // ../engine/src/logger.js
47989
+ // ../engine/src/logger.ts
47990
+ function withSeverityMarker2(level, payload) {
47991
+ return `${LOG_LEVEL_MARKER_PREFIX2}${level}${LOG_LEVEL_MARKER_SUFFIX2}${payload}`;
47992
+ }
47631
47993
  function createLogger2(prefix) {
47632
47994
  const tag = `[${prefix}]`;
47633
47995
  return {
47634
47996
  log(message, ...args) {
47635
- globalThis.console.error(`${tag} ${message}`, ...args);
47997
+ console.error(withSeverityMarker2("info", `${tag} ${message}`), ...args);
47636
47998
  },
47637
47999
  warn(message, ...args) {
47638
- globalThis.console.warn(`${tag} ${message}`, ...args);
48000
+ console.warn(withSeverityMarker2("warn", `${tag} ${message}`), ...args);
47639
48001
  },
47640
48002
  error(message, ...args) {
47641
- globalThis.console.error(`${tag} ${message}`, ...args);
48003
+ console.error(withSeverityMarker2("error", `${tag} ${message}`), ...args);
47642
48004
  }
47643
48005
  };
47644
48006
  }
47645
- var schedulerLog, executorLog, triageLog, piLog, extensionsLog, mergerLog, worktreePoolLog, reviewerLog, prMonitorLog, runtimeLog, ipcLog, projectManagerLog, hybridExecutorLog, autopilotLog, heartbeatLog, remoteNodeLog, nodeHealthMonitorLog, peerExchangeLog;
48007
+ function formatError(err) {
48008
+ if (err instanceof Error) {
48009
+ const message2 = err.message || err.name || "Error";
48010
+ const stack = err.stack;
48011
+ const detail = stack && stack.includes(message2) ? stack : stack ? `${message2}
48012
+ ${stack}` : message2;
48013
+ return { message: message2, stack, detail };
48014
+ }
48015
+ let message;
48016
+ if (typeof err === "string") {
48017
+ message = err;
48018
+ } else {
48019
+ try {
48020
+ message = JSON.stringify(err);
48021
+ } catch {
48022
+ message = String(err);
48023
+ }
48024
+ }
48025
+ return { message, detail: message };
48026
+ }
48027
+ var LOG_LEVEL_MARKER_PREFIX2, LOG_LEVEL_MARKER_SUFFIX2, schedulerLog, executorLog, triageLog, piLog, extensionsLog, mergerLog, worktreePoolLog, reviewerLog, prMonitorLog, runtimeLog, ipcLog, projectManagerLog, hybridExecutorLog, autopilotLog, heartbeatLog, remoteNodeLog, nodeHealthMonitorLog, peerExchangeLog;
47646
48028
  var init_logger2 = __esm({
47647
- "../engine/src/logger.js"() {
48029
+ "../engine/src/logger.ts"() {
47648
48030
  "use strict";
48031
+ LOG_LEVEL_MARKER_PREFIX2 = "\0fnlvl=";
48032
+ LOG_LEVEL_MARKER_SUFFIX2 = "\0";
47649
48033
  schedulerLog = createLogger2("scheduler");
47650
48034
  executorLog = createLogger2("executor");
47651
48035
  triageLog = createLogger2("triage");
@@ -48100,7 +48484,7 @@ async function getAgentMemoryWindow(rootDir, agentMemory, path, startLine = 1, l
48100
48484
  }
48101
48485
  function createTaskCreateTool(store) {
48102
48486
  return {
48103
- name: "task_create",
48487
+ name: "fn_task_create",
48104
48488
  label: "Create Task",
48105
48489
  description: "Create a new task for out-of-scope work discovered during execution. The task goes into triage where it will be specified by the AI. Optionally set dependencies (e.g., the new task depends on the current one, or the current task should wait for the new one).",
48106
48490
  parameters: taskCreateParams,
@@ -48123,7 +48507,7 @@ function createTaskCreateTool(store) {
48123
48507
  }
48124
48508
  function createTaskLogTool(store, taskId) {
48125
48509
  return {
48126
- name: "task_log",
48510
+ name: "fn_task_log",
48127
48511
  label: "Log Entry",
48128
48512
  description: "Log an important action, decision, or issue for this task. Use for significant events \u2014 not every small step.",
48129
48513
  parameters: taskLogParams,
@@ -48138,7 +48522,7 @@ function createTaskLogTool(store, taskId) {
48138
48522
  }
48139
48523
  function createTaskLogToolWithContext(store, taskId, runContext) {
48140
48524
  return {
48141
- name: "task_log",
48525
+ name: "fn_task_log",
48142
48526
  label: "Log Entry",
48143
48527
  description: "Log an important action, decision, or issue for this task. Use for significant events \u2014 not every small step.",
48144
48528
  parameters: taskLogParams,
@@ -48153,7 +48537,7 @@ function createTaskLogToolWithContext(store, taskId, runContext) {
48153
48537
  }
48154
48538
  function createTaskDocumentWriteTool(store, taskId) {
48155
48539
  return {
48156
- name: "task_document_write",
48540
+ name: "fn_task_document_write",
48157
48541
  label: "Write Document",
48158
48542
  description: "Save a named document for this task (for example plan, notes, or research). Each write creates a new revision so you can update documents over time.",
48159
48543
  parameters: taskDocumentWriteParams,
@@ -48186,7 +48570,7 @@ function createTaskDocumentWriteTool(store, taskId) {
48186
48570
  }
48187
48571
  function createTaskDocumentReadTool(store, taskId) {
48188
48572
  return {
48189
- name: "task_document_read",
48573
+ name: "fn_task_document_read",
48190
48574
  label: "Read Document",
48191
48575
  description: "Read a named document for this task, or list all documents when no key is provided.",
48192
48576
  parameters: taskDocumentReadParams,
@@ -48242,9 +48626,9 @@ ${lines.join("\n")}`
48242
48626
  }
48243
48627
  function createMemorySearchTool(rootDir, settings, options) {
48244
48628
  return {
48245
- name: "memory_search",
48629
+ name: "fn_memory_search",
48246
48630
  label: "Search Memory",
48247
- description: "Search durable project memory and this agent's own memory, returning small snippets with file paths and line ranges. Use this before memory_get; do not read all memory by default.",
48631
+ description: "Search durable project memory and this agent's own memory, returning small snippets with file paths and line ranges. Use this before fn_memory_get; do not read all memory by default.",
48248
48632
  parameters: memorySearchParams,
48249
48633
  execute: async (_id, params) => {
48250
48634
  const limit = params.limit ?? 5;
@@ -48270,9 +48654,9 @@ function createMemorySearchTool(rootDir, settings, options) {
48270
48654
  }
48271
48655
  function createMemoryGetTool(rootDir, settings, options) {
48272
48656
  return {
48273
- name: "memory_get",
48657
+ name: "fn_memory_get",
48274
48658
  label: "Get Memory",
48275
- description: "Read a bounded line window from a memory file returned by memory_search. Allowed files include project memory under .fusion/memory/ and this agent's own .fusion/agent-memory/{agentId}/MEMORY.md file.",
48659
+ description: "Read a bounded line window from a memory file returned by fn_memory_search. Allowed files include project memory under .fusion/memory/ and this agent's own .fusion/agent-memory/{agentId}/MEMORY.md file.",
48276
48660
  parameters: memoryGetParams,
48277
48661
  execute: async (_id, params) => {
48278
48662
  const agentResult = options?.agentMemory ? await getAgentMemoryWindow(rootDir, options.agentMemory, params.path, params.startLine, params.lineCount) : null;
@@ -48306,7 +48690,7 @@ ${result.content}`
48306
48690
  }
48307
48691
  function createMemoryAppendTool(rootDir, settings, options) {
48308
48692
  return {
48309
- name: "memory_append",
48693
+ name: "fn_memory_append",
48310
48694
  label: "Append Memory",
48311
48695
  description: "Append concise Markdown to project memory. Use long-term only for durable conventions/decisions/pitfalls; use daily for running observations and open loops. Skip this tool when there is no reusable memory.",
48312
48696
  parameters: memoryAppendParams,
@@ -48368,7 +48752,7 @@ function createMemoryTools(rootDir, settings, options) {
48368
48752
  }
48369
48753
  function createReflectOnPerformanceTool(reflectionService, agentId) {
48370
48754
  return {
48371
- name: "reflect_on_performance",
48755
+ name: "fn_reflect_on_performance",
48372
48756
  label: "Reflect on Performance",
48373
48757
  description: 'Review your past task performance and generate insights for improvement. Optionally focus on a specific area like "code quality", "speed", or "testing".',
48374
48758
  parameters: reflectOnPerformanceParams,
@@ -48401,7 +48785,7 @@ function createReflectOnPerformanceTool(reflectionService, agentId) {
48401
48785
  }
48402
48786
  function createListAgentsTool(agentStore) {
48403
48787
  return {
48404
- name: "list_agents",
48788
+ name: "fn_list_agents",
48405
48789
  label: "List Agents",
48406
48790
  description: "List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. Use this to discover which agents exist and what they specialize in before delegating work.",
48407
48791
  parameters: listAgentsParams,
@@ -48444,9 +48828,9 @@ ${lines.join("\n\n")}` }],
48444
48828
  }
48445
48829
  function createDelegateTaskTool(agentStore, taskStore) {
48446
48830
  return {
48447
- name: "delegate_task",
48831
+ name: "fn_delegate_task",
48448
48832
  label: "Delegate Task",
48449
- description: "Create a new task and assign it to a specific agent for execution. The task goes to 'todo' and will be picked up by the target agent on their next heartbeat cycle. Use list_agents first to find available agents and their capabilities.",
48833
+ description: "Create a new task and assign it to a specific agent for execution. The task goes to 'todo' and will be picked up by the target agent on their next heartbeat cycle. Use fn_list_agents first to find available agents and their capabilities.",
48450
48834
  parameters: delegateTaskParams,
48451
48835
  execute: async (_id, params) => {
48452
48836
  const agent = await agentStore.getAgent(params.agent_id);
@@ -48481,7 +48865,7 @@ function createDelegateTaskTool(agentStore, taskStore) {
48481
48865
  }
48482
48866
  function createSendMessageTool(messageStore, fromAgentId) {
48483
48867
  return {
48484
- name: "send_message",
48868
+ name: "fn_send_message",
48485
48869
  label: "Send Message",
48486
48870
  description: "Send a message to another agent or user. The recipient will be woken if they have `messageResponseMode: 'immediate'` configured. When replying to an existing message, include `reply_to_message_id` to preserve threading.",
48487
48871
  parameters: sendMessageParams,
@@ -48538,7 +48922,7 @@ function createSendMessageTool(messageStore, fromAgentId) {
48538
48922
  }
48539
48923
  function createReadMessagesTool(messageStore, agentId) {
48540
48924
  return {
48541
- name: "read_messages",
48925
+ name: "fn_read_messages",
48542
48926
  label: "Read Messages",
48543
48927
  description: "Read your inbox messages. Returns unread messages by default.",
48544
48928
  parameters: readMessagesParams,
@@ -48640,7 +49024,7 @@ var init_agent_tools = __esm({
48640
49024
  Type.Literal("agent-to-user")
48641
49025
  ], { description: "Message type (defaults to 'agent-to-agent')" })),
48642
49026
  reply_to_message_id: Type.Optional(
48643
- Type.String({ description: "Optional ID of the message you are replying to (use IDs from read_messages output)" })
49027
+ Type.String({ description: "Optional ID of the message you are replying to (use IDs from fn_read_messages output)" })
48644
49028
  )
48645
49029
  });
48646
49030
  readMessagesParams = Type.Object({
@@ -48652,7 +49036,7 @@ var init_agent_tools = __esm({
48652
49036
  limit: Type.Optional(Type.Number({ description: "Maximum snippets to return (default: 5, max: 20)" }))
48653
49037
  });
48654
49038
  memoryGetParams = Type.Object({
48655
- path: Type.String({ description: "Memory path from memory_search, e.g. .fusion/memory/MEMORY.md or .fusion/memory/YYYY-MM-DD.md" }),
49039
+ path: Type.String({ description: "Memory path from fn_memory_search, e.g. .fusion/memory/MEMORY.md or .fusion/memory/YYYY-MM-DD.md" }),
48656
49040
  startLine: Type.Optional(Type.Number({ description: "1-based start line (default: 1)" })),
48657
49041
  lineCount: Type.Optional(Type.Number({ description: "Number of lines to read (default: 120, max: 400)" }))
48658
49042
  });
@@ -48801,7 +49185,7 @@ var init_concurrency = __esm({
48801
49185
  }
48802
49186
  });
48803
49187
 
48804
- // ../engine/src/skill-resolver.js
49188
+ // ../engine/src/skill-resolver.ts
48805
49189
  import { existsSync as existsSync18, readFileSync as readFileSync4 } from "node:fs";
48806
49190
  import { join as join22 } from "node:path";
48807
49191
  function readJsonObject(path) {
@@ -48925,9 +49309,13 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
48925
49309
  let filteredSkills;
48926
49310
  if (hasRequestedNames) {
48927
49311
  const requestedNamesLower = new Set(requestedSkillNames.map((n) => n.toLowerCase()));
48928
- filteredSkills = base.skills.filter((skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath));
49312
+ filteredSkills = base.skills.filter(
49313
+ (skill) => requestedNamesLower.has(skill.name.toLowerCase()) && !excludedSkillPaths.has(skill.filePath)
49314
+ );
48929
49315
  } else if (hasPatterns) {
48930
- filteredSkills = base.skills.filter((skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath));
49316
+ filteredSkills = base.skills.filter(
49317
+ (skill) => allowedSkillPaths.has(skill.filePath) && !excludedSkillPaths.has(skill.filePath)
49318
+ );
48931
49319
  } else if (hasExcluded) {
48932
49320
  filteredSkills = base.skills.filter((skill) => !excludedSkillPaths.has(skill.filePath));
48933
49321
  } else {
@@ -48967,6 +49355,7 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
48967
49355
  }
48968
49356
  }
48969
49357
  if (newDiagnostics.length > 0) {
49358
+ const _purpose = sessionPurpose ? `[${sessionPurpose}]` : "skills";
48970
49359
  for (const diag of newDiagnostics) {
48971
49360
  piLog.warn(`[skills] ${diag.type}: ${diag.message}`);
48972
49361
  }
@@ -48978,21 +49367,20 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
48978
49367
  };
48979
49368
  }
48980
49369
  var init_skill_resolver = __esm({
48981
- "../engine/src/skill-resolver.js"() {
49370
+ "../engine/src/skill-resolver.ts"() {
48982
49371
  "use strict";
48983
49372
  init_logger2();
48984
49373
  }
48985
49374
  });
48986
49375
 
48987
- // ../engine/src/context-limit-detector.js
49376
+ // ../engine/src/context-limit-detector.ts
48988
49377
  function isContextLimitError(message) {
48989
- if (!message)
48990
- return false;
49378
+ if (!message) return false;
48991
49379
  return CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(message));
48992
49380
  }
48993
49381
  var CONTEXT_OVERFLOW_PATTERNS;
48994
49382
  var init_context_limit_detector = __esm({
48995
- "../engine/src/context-limit-detector.js"() {
49383
+ "../engine/src/context-limit-detector.ts"() {
48996
49384
  "use strict";
48997
49385
  CONTEXT_OVERFLOW_PATTERNS = [
48998
49386
  // Anthropic: "prompt is too long: X tokens > Y maximum"
@@ -49029,14 +49417,14 @@ var init_context_limit_detector = __esm({
49029
49417
  }
49030
49418
  });
49031
49419
 
49032
- // ../engine/src/auth-storage.js
49420
+ // ../engine/src/auth-storage.ts
49033
49421
  import { existsSync as existsSync19, readFileSync as readFileSync5 } from "node:fs";
49034
49422
  import { homedir as homedir4 } from "node:os";
49035
49423
  import { join as join23 } from "node:path";
49036
49424
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
49037
49425
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
49038
49426
  function getHomeDir2() {
49039
- return globalThis.process.env.HOME || globalThis.process.env.USERPROFILE || homedir4();
49427
+ return process.env.HOME || process.env.USERPROFILE || homedir4();
49040
49428
  }
49041
49429
  function getFusionAuthPath(home = getHomeDir2()) {
49042
49430
  return join23(home, ".fusion", "agent", "auth.json");
@@ -49080,9 +49468,8 @@ function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
49080
49468
  return credentials;
49081
49469
  }
49082
49470
  function resolveStoredApiKey(key) {
49083
- if (!key)
49084
- return void 0;
49085
- return globalThis.process.env[key] ?? key;
49471
+ if (!key) return void 0;
49472
+ return process.env[key] ?? key;
49086
49473
  }
49087
49474
  function resolveOAuthApiKey(providerId, credential) {
49088
49475
  if (credential.type !== "oauth" || typeof credential.access !== "string" || typeof credential.refresh !== "string" || typeof credential.expires !== "number" || Date.now() >= credential.expires) {
@@ -49128,8 +49515,7 @@ function createFusionAuthStorage() {
49128
49515
  if (prop === "getApiKey") {
49129
49516
  return async (provider) => {
49130
49517
  const primaryKey = await target.getApiKey(provider);
49131
- if (primaryKey)
49132
- return primaryKey;
49518
+ if (primaryKey) return primaryKey;
49133
49519
  return resolveStoredCredentialApiKey(provider, legacyCredentials[provider]);
49134
49520
  };
49135
49521
  }
@@ -49138,12 +49524,12 @@ function createFusionAuthStorage() {
49138
49524
  });
49139
49525
  }
49140
49526
  var init_auth_storage = __esm({
49141
- "../engine/src/auth-storage.js"() {
49527
+ "../engine/src/auth-storage.ts"() {
49142
49528
  "use strict";
49143
49529
  }
49144
49530
  });
49145
49531
 
49146
- // ../engine/src/pi.js
49532
+ // ../engine/src/pi.ts
49147
49533
  var pi_exports = {};
49148
49534
  __export(pi_exports, {
49149
49535
  COMPACTION_FALLBACK_INSTRUCTIONS: () => COMPACTION_FALLBACK_INSTRUCTIONS,
@@ -49156,20 +49542,36 @@ __export(pi_exports, {
49156
49542
  import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
49157
49543
  import { exec } from "node:child_process";
49158
49544
  import { promisify as promisify2 } from "node:util";
49159
- import { basename as basename6, dirname as dirname6, join as join24, relative as relative3, isAbsolute as isAbsolute6, resolve as resolve10 } from "node:path";
49160
- import { createAgentSession, createCodingTools, createExtensionRuntime, createReadOnlyTools, DefaultResourceLoader, DefaultPackageManager, discoverAndLoadExtensions, ModelRegistry, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
49545
+ import { basename as basename7, dirname as dirname7, join as join24, relative as relative3, isAbsolute as isAbsolute6, resolve as resolve10 } from "node:path";
49546
+ import {
49547
+ createAgentSession,
49548
+ createCodingTools,
49549
+ createExtensionRuntime,
49550
+ createReadOnlyTools,
49551
+ DefaultResourceLoader,
49552
+ DefaultPackageManager,
49553
+ discoverAndLoadExtensions,
49554
+ ModelRegistry,
49555
+ SessionManager,
49556
+ SettingsManager
49557
+ } from "@mariozechner/pi-coding-agent";
49161
49558
  function getSessionStateError(session) {
49162
- const error = session.state?.error;
49559
+ const state = session.state;
49560
+ const error = state?.errorMessage ?? state?.error;
49163
49561
  return typeof error === "string" ? error : "";
49164
49562
  }
49165
49563
  function clearSessionStateError(session) {
49166
49564
  const state = session.state;
49167
- if (!state || typeof state !== "object" || !("error" in state)) {
49565
+ if (!state || typeof state !== "object") {
49168
49566
  return;
49169
49567
  }
49170
- try {
49171
- state.error = void 0;
49172
- } catch {
49568
+ for (const key of ["errorMessage", "error"]) {
49569
+ if (key in state) {
49570
+ try {
49571
+ state[key] = void 0;
49572
+ } catch {
49573
+ }
49574
+ }
49173
49575
  }
49174
49576
  }
49175
49577
  async function promptSessionAndCheck(session, prompt, options) {
@@ -49181,6 +49583,29 @@ async function promptSessionAndCheck(session, prompt, options) {
49181
49583
  }
49182
49584
  const stateError = getSessionStateError(session);
49183
49585
  if (stateError) {
49586
+ if (/Cannot read propert(y|ies) of (undefined|null)/i.test(stateError)) {
49587
+ try {
49588
+ const messages = session.agent?.state?.messages ?? session.state?.messages;
49589
+ if (Array.isArray(messages)) {
49590
+ const recent = messages.slice(-6).map((m, idx) => {
49591
+ const i = messages.length - 6 + idx;
49592
+ const content = m?.content;
49593
+ return {
49594
+ index: i < 0 ? idx : i,
49595
+ role: m?.role,
49596
+ contentType: Array.isArray(content) ? `array(len=${content.length})` : typeof content,
49597
+ toolName: m.toolName,
49598
+ stopReason: m.stopReason
49599
+ };
49600
+ });
49601
+ piLog.error(`pi state error \u2014 transcript tail (${messages.length} msgs total): ${JSON.stringify(recent)}`);
49602
+ } else {
49603
+ piLog.error(`pi state error \u2014 state.messages is not an array: ${typeof messages}`);
49604
+ }
49605
+ } catch (inspectErr) {
49606
+ piLog.warn(`pi state error \u2014 failed to inspect transcript: ${inspectErr instanceof Error ? inspectErr.message : String(inspectErr)}`);
49607
+ }
49608
+ }
49184
49609
  throw new Error(stateError);
49185
49610
  }
49186
49611
  }
@@ -49232,8 +49657,7 @@ async function promptWithFallback(session, prompt, options) {
49232
49657
  }
49233
49658
  function describeModel(session) {
49234
49659
  const model = session.model;
49235
- if (!model)
49236
- return "unknown model";
49660
+ if (!model) return "unknown model";
49237
49661
  return `${model.provider}/${model.id}`;
49238
49662
  }
49239
49663
  function compactMarkdownMemorySection(sectionBody) {
@@ -49286,7 +49710,9 @@ async function retryWithCompactedPromptMemory(session, prompt, options) {
49286
49710
  if (!compactedPrompt) {
49287
49711
  return { recovered: false };
49288
49712
  }
49289
- piLog.log(`promptWithFallback: retrying with compacted prompt memory (${prompt.length} \u2192 ${compactedPrompt.length} chars)`);
49713
+ piLog.log(
49714
+ `promptWithFallback: retrying with compacted prompt memory (${prompt.length} \u2192 ${compactedPrompt.length} chars)`
49715
+ );
49290
49716
  try {
49291
49717
  await promptSessionAndCheck(session, compactedPrompt, options);
49292
49718
  piLog.log("promptWithFallback: prompt completed after prompt-memory compaction");
@@ -49303,7 +49729,7 @@ async function flushMemoryBeforeSessionCompaction(session) {
49303
49729
  }
49304
49730
  const flushPrompt = [
49305
49731
  "Before context compaction, preserve only unresolved durable memory if needed.",
49306
- "If memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
49732
+ "If fn_memory_append is available and you learned reusable project decisions, conventions, pitfalls, or open loops that are not already saved, append them now.",
49307
49733
  'Use layer="long-term" for durable facts and layer="daily" for running notes/open loops.',
49308
49734
  "If there is nothing durable to save, reply exactly: NONE."
49309
49735
  ].join("\n");
@@ -49348,7 +49774,9 @@ function resolveConfiguredModel(modelRegistry, kind, provider, modelId) {
49348
49774
  piLog.warn(`${kind} model ${provider}/${modelId} not in registry; using provider base model as template`);
49349
49775
  return { ...baseModel, id: modelId, name: modelId };
49350
49776
  }
49351
- throw new Error(`Configured ${kind} model ${provider}/${modelId} was not found in the pi model registry. Open Settings and choose a model from /api/models, or update your pi model configuration.`);
49777
+ throw new Error(
49778
+ `Configured ${kind} model ${provider}/${modelId} was not found in the pi model registry. Open Settings and choose a model from /api/models, or update your pi model configuration.`
49779
+ );
49352
49780
  }
49353
49781
  function isRetryableModelSelectionError(message) {
49354
49782
  const normalized = message.toLowerCase();
@@ -49393,11 +49821,18 @@ function normalizeAssistantOrToolResultMessage(message) {
49393
49821
  return false;
49394
49822
  }
49395
49823
  const role = message.role;
49396
- if (role !== "assistant" && role !== "toolResult") {
49824
+ if (role !== "assistant" && role !== "toolResult" && role !== "user") {
49397
49825
  return false;
49398
49826
  }
49399
- if (!Array.isArray(message.content)) {
49400
- message.content = [];
49827
+ const obj = message;
49828
+ if (role === "user") {
49829
+ if (typeof obj.content !== "string" && !Array.isArray(obj.content)) {
49830
+ obj.content = [];
49831
+ }
49832
+ return true;
49833
+ }
49834
+ if (!Array.isArray(obj.content)) {
49835
+ obj.content = [];
49401
49836
  }
49402
49837
  return true;
49403
49838
  }
@@ -49454,6 +49889,12 @@ function installMessageContentGuard(session, sessionManager) {
49454
49889
  if (session.__fusionMessageContentGuardInstalled) {
49455
49890
  return;
49456
49891
  }
49892
+ const existingMessages = session.agent?.state?.messages;
49893
+ if (Array.isArray(existingMessages)) {
49894
+ for (const candidate of existingMessages) {
49895
+ normalizeAssistantOrToolResultMessage(candidate);
49896
+ }
49897
+ }
49457
49898
  if (typeof session.subscribe === "function") {
49458
49899
  session.subscribe((event) => {
49459
49900
  if (!event || typeof event !== "object" || event.type !== "message_end") {
@@ -49480,10 +49921,10 @@ function hasPackageManagerSettings(settings) {
49480
49921
  return Array.isArray(settings.packages) || Array.isArray(settings.npmCommand);
49481
49922
  }
49482
49923
  function siblingAgentDir(agentDir, siblingRoot) {
49483
- if (basename6(agentDir) !== "agent") {
49924
+ if (basename7(agentDir) !== "agent") {
49484
49925
  return void 0;
49485
49926
  }
49486
- return join24(dirname6(dirname6(agentDir)), siblingRoot, "agent");
49927
+ return join24(dirname7(dirname7(agentDir)), siblingRoot, "agent");
49487
49928
  }
49488
49929
  function createReadOnlyPiSettingsView(cwd, agentDir) {
49489
49930
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
@@ -49496,8 +49937,8 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
49496
49937
  const fusionProjectSettings = readJsonObject2(join24(projectRoot, ".fusion", "settings.json"));
49497
49938
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
49498
49939
  return {
49499
- getGlobalSettings: () => globalThis.structuredClone(globalSettings),
49500
- getProjectSettings: () => globalThis.structuredClone(fusionProjectSettings),
49940
+ getGlobalSettings: () => structuredClone(globalSettings),
49941
+ getProjectSettings: () => structuredClone(fusionProjectSettings),
49501
49942
  getNpmCommand: () => Array.isArray(mergedSettings.npmCommand) ? [...mergedSettings.npmCommand] : void 0
49502
49943
  };
49503
49944
  }
@@ -49524,7 +49965,11 @@ async function registerExtensionProviders(cwd, modelRegistry) {
49524
49965
  });
49525
49966
  const resolvedPaths = await packageManager.resolve();
49526
49967
  const packageExtensionPaths = resolvedPaths.extensions.filter((resource) => resource.enabled).map((resource) => resource.path);
49527
- const extensionsResult = await discoverAndLoadExtensions([...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths], cwd, join24(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery"));
49968
+ const extensionsResult = await discoverAndLoadExtensions(
49969
+ [...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
49970
+ cwd,
49971
+ join24(resolvePiExtensionProjectRoot(cwd), ".fusion", "disabled-auto-extension-discovery")
49972
+ );
49528
49973
  for (const { path, error } of extensionsResult.errors) {
49529
49974
  extensionsLog.warn(`Failed to load ${path}: ${error}`);
49530
49975
  }
@@ -49559,7 +50004,9 @@ async function isRegisteredGitWorktree(projectRoot, worktreePath) {
49559
50004
  encoding: "utf-8"
49560
50005
  });
49561
50006
  const resolvedWorktree = resolve10(worktreePath);
49562
- return stdout.split("\n").some((line) => line.startsWith("worktree ") && resolve10(line.slice("worktree ".length)) === resolvedWorktree);
50007
+ return stdout.split("\n").some(
50008
+ (line) => line.startsWith("worktree ") && resolve10(line.slice("worktree ".length)) === resolvedWorktree
50009
+ );
49563
50010
  } catch {
49564
50011
  return false;
49565
50012
  }
@@ -49586,7 +50033,7 @@ async function assertValidWorktreeSession(cwd, projectRoot) {
49586
50033
  throw new Error(`Refusing to start coding agent in unregistered git worktree: ${cwd}`);
49587
50034
  }
49588
50035
  }
49589
- function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath) {
50036
+ function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath, toolName) {
49590
50037
  const worktreeResolved = resolve10(worktreePath);
49591
50038
  const projectRootResolved = resolve10(projectRoot);
49592
50039
  const requestedResolved = isAbsolute6(requestedPath) ? resolve10(requestedPath) : resolve10(worktreeResolved, requestedPath);
@@ -49601,8 +50048,20 @@ function isWorktreeAllowedPath(worktreePath, projectRoot, requestedPath) {
49601
50048
  if (relToProjectRoot.match(/^\.fusion\/tasks\/[^/]+\/attachments\//)) {
49602
50049
  return true;
49603
50050
  }
50051
+ const readOnlyTools = /* @__PURE__ */ new Set(["read", "glob", "grep"]);
50052
+ if (toolName && readOnlyTools.has(toolName) && /^\.fusion\/tasks\/[^/]+\/(PROMPT\.md|task\.json)$/.test(relToProjectRoot)) {
50053
+ return true;
50054
+ }
49604
50055
  return false;
49605
50056
  }
50057
+ function boundaryRejection(message) {
50058
+ return {
50059
+ content: [{ type: "text", text: message }],
50060
+ isError: true,
50061
+ ok: false,
50062
+ error: message
50063
+ };
50064
+ }
49606
50065
  function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
49607
50066
  if (!worktreePath || !projectRoot) {
49608
50067
  return tools;
@@ -49616,21 +50075,21 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
49616
50075
  return {
49617
50076
  ...tool,
49618
50077
  execute: async (...args) => {
50078
+ const _toolCallId = args[0];
49619
50079
  const params = args[1];
50080
+ const _signal = args[2];
49620
50081
  const pathArg = params.path;
49621
- if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg)) {
50082
+ if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg, tool.name)) {
49622
50083
  const relToProject = relative3(projectRoot, pathArg);
49623
- return {
49624
- ok: false,
49625
- error: `Path "${relToProject}" is outside the worktree boundary. Coding agents can only modify files inside the current worktree. Exception: .fusion/memory/ (project root) and .fusion/tasks/*/attachments/* are permitted for reading.`
49626
- };
50084
+ return boundaryRejection(
50085
+ `Path "${relToProject}" is outside the worktree boundary. Coding agents can only modify files inside the current worktree. Exceptions (read-only): .fusion/memory/, .fusion/tasks/*/attachments/, and .fusion/tasks/*/{PROMPT.md,task.json} for dependency context.`
50086
+ );
49627
50087
  }
49628
50088
  const cwdArg = params.cwd;
49629
- if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg)) {
49630
- return {
49631
- ok: false,
49632
- error: `Working directory is outside the worktree boundary. Commands must run inside the worktree.`
49633
- };
50089
+ if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg, tool.name)) {
50090
+ return boundaryRejection(
50091
+ `Working directory is outside the worktree boundary. Commands must run inside the worktree.`
50092
+ );
49634
50093
  }
49635
50094
  return originalExecute(...args);
49636
50095
  }
@@ -49640,7 +50099,7 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
49640
50099
  async function createFnAgent2(options) {
49641
50100
  piLog.log(`createFnAgent called (cwd=${options.cwd}, tools=${options.tools}, provider=${options.defaultProvider}, model=${options.defaultModelId})`);
49642
50101
  const authStorage = createFusionAuthStorage();
49643
- const modelRegistry = new ModelRegistry(authStorage, getModelRegistryModelsPath());
50102
+ const modelRegistry = ModelRegistry.create(authStorage, getModelRegistryModelsPath());
49644
50103
  await registerExtensionProviders(options.cwd, modelRegistry);
49645
50104
  const tools = options.tools === "readonly" ? createReadOnlyTools(options.cwd) : createCodingTools(options.cwd);
49646
50105
  const worktreePath = options.cwd;
@@ -49653,8 +50112,18 @@ async function createFnAgent2(options) {
49653
50112
  compaction: { enabled: true },
49654
50113
  retry: { enabled: true, maxRetries: 3 }
49655
50114
  });
49656
- const selectedModel = resolveConfiguredModel(modelRegistry, "primary", options.defaultProvider, options.defaultModelId);
49657
- const fallbackModel = resolveConfiguredModel(modelRegistry, "fallback", options.fallbackProvider, options.fallbackModelId);
50115
+ const selectedModel = resolveConfiguredModel(
50116
+ modelRegistry,
50117
+ "primary",
50118
+ options.defaultProvider,
50119
+ options.defaultModelId
50120
+ );
50121
+ const fallbackModel = resolveConfiguredModel(
50122
+ modelRegistry,
50123
+ "fallback",
50124
+ options.fallbackProvider,
50125
+ options.fallbackModelId
50126
+ );
49658
50127
  let effectiveSkillSelection = options.skillSelection;
49659
50128
  if (!effectiveSkillSelection && options.skills && options.skills.length > 0) {
49660
50129
  piLog.log(`Using skills from convenience parameter: [${options.skills.join(", ")}]`);
@@ -49680,6 +50149,7 @@ async function createFnAgent2(options) {
49680
50149
  }
49681
50150
  const resourceLoader = new DefaultResourceLoader({
49682
50151
  cwd: options.cwd,
50152
+ agentDir: getFusionAgentDir(),
49683
50153
  settingsManager,
49684
50154
  systemPromptOverride: () => options.systemPrompt,
49685
50155
  appendSystemPromptOverride: () => [],
@@ -49689,13 +50159,17 @@ async function createFnAgent2(options) {
49689
50159
  const sessionManager = options.sessionManager ?? SessionManager.inMemory();
49690
50160
  normalizeSessionHistoryEntries(sessionManager);
49691
50161
  const createSessionWithModel = async (modelOverride) => {
50162
+ const customToolList = [
50163
+ ...wrappedTools,
50164
+ ...options.customTools ?? []
50165
+ ];
49692
50166
  return createAgentSession({
49693
50167
  cwd: options.cwd,
49694
50168
  authStorage,
49695
50169
  modelRegistry,
49696
50170
  resourceLoader,
49697
- tools: wrappedTools,
49698
- customTools: options.customTools,
50171
+ noTools: "builtin",
50172
+ customTools: customToolList,
49699
50173
  sessionManager,
49700
50174
  settingsManager,
49701
50175
  ...modelOverride ? { model: modelOverride } : {}
@@ -49719,7 +50193,7 @@ async function createFnAgent2(options) {
49719
50193
  const { session } = sessionResult;
49720
50194
  installToolResultContentGuard(session);
49721
50195
  installMessageContentGuard(session, sessionManager);
49722
- session.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
50196
+ session.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
49723
50197
  const promptableSession = session;
49724
50198
  promptableSession.promptWithFallback = async (prompt, promptOptions) => {
49725
50199
  try {
@@ -49769,8 +50243,11 @@ async function createFnAgent2(options) {
49769
50243
  const fallbackSessionResult = await createSessionWithModel(fallbackModel);
49770
50244
  const fallbackSession = fallbackSessionResult.session;
49771
50245
  installToolResultContentGuard(fallbackSession);
49772
- installMessageContentGuard(fallbackSession, sessionManager);
49773
- fallbackSession.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === "memory_append") === true;
50246
+ installMessageContentGuard(
50247
+ fallbackSession,
50248
+ sessionManager
50249
+ );
50250
+ fallbackSession.__fusionMemoryAppendAvailable = options.customTools?.some((tool) => tool.name === FN_MEMORY_APPEND_TOOL_NAME) === true;
49774
50251
  if (options.defaultThinkingLevel) {
49775
50252
  fallbackSession.setThinkingLevel(options.defaultThinkingLevel);
49776
50253
  }
@@ -49852,9 +50329,9 @@ async function createFnAgent2(options) {
49852
50329
  });
49853
50330
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
49854
50331
  }
49855
- var execAsync, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS;
50332
+ var execAsync, FN_MEMORY_APPEND_TOOL_NAME, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS;
49856
50333
  var init_pi = __esm({
49857
- "../engine/src/pi.js"() {
50334
+ "../engine/src/pi.ts"() {
49858
50335
  "use strict";
49859
50336
  init_src();
49860
50337
  init_skill_resolver();
@@ -49862,6 +50339,7 @@ var init_pi = __esm({
49862
50339
  init_auth_storage();
49863
50340
  init_logger2();
49864
50341
  execAsync = promisify2(exec);
50342
+ FN_MEMORY_APPEND_TOOL_NAME = "fn_memory_append";
49865
50343
  COMPACTION_FALLBACK_INSTRUCTIONS = [
49866
50344
  "Summarize all completed steps concisely.",
49867
50345
  "Preserve the current step number and any in-progress work details.",
@@ -50802,12 +51280,12 @@ When reviewing specs, actively assess whether the task should have been broken i
50802
51280
  Say explicitly: "This task should be broken into subtasks because [specific reason]."
50803
51281
  Recommend the number of child tasks (2-5) and what each should cover.
50804
51282
  **Critically**, instruct the planner to take these actions in your REVISE feedback:
50805
- 1. Use the \`task_create\` tool to create 2\u20135 child tasks from the oversized spec
51283
+ 1. Use the \`fn_task_create\` tool to create 2\u20135 child tasks from the oversized spec
50806
51284
  2. Do NOT write a parent PROMPT.md \u2014 the parent will be closed automatically after children are created
50807
51285
  3. Each child task should cover one coherent deliverable with clear scope boundaries
50808
51286
 
50809
51287
  Example REVISE feedback for an undersplit task:
50810
- "This task should be broken into 3 subtasks because it spans the engine, dashboard, and CLI packages with independent deliverables. Use task_create to create: (1) engine logic, (2) dashboard UI, (3) CLI integration. Do not write a parent PROMPT."
51288
+ "This task should be broken into 3 subtasks because it spans the engine, dashboard, and CLI packages with independent deliverables. Use fn_task_create to create: (1) engine logic, (2) dashboard UI, (3) CLI integration. Do not write a parent PROMPT."
50811
51289
 
50812
51290
  **Do NOT flag if:**
50813
51291
  - Steps are sequential and tightly coupled (e.g., a pipeline where each step depends on the previous)
@@ -51180,12 +51658,12 @@ The user has requested that this task be broken into smaller subtasks if it is c
51180
51658
 
51181
51659
  **How to split:**
51182
51660
  1. First, analyze the task to determine if it should be split
51183
- 2. If splitting: use the \\\`task_create\\\` tool to create child tasks in order, setting up dependencies as needed
51661
+ 2. If splitting: use the \\\`fn_task_create\\\` tool to create child tasks in order, setting up dependencies as needed
51184
51662
  3. Include clear descriptions and acceptance criteria for each child task
51185
51663
  4. After creating all subtasks, stop \u2014 do NOT write a PROMPT.md for the parent task
51186
51664
  5. If NOT splitting: proceed with a normal PROMPT.md specification for this task
51187
51665
 
51188
- **Subtask dependencies rule:** \`dependencies\` on a child may only reference **sibling subtasks created earlier in this same split** or **pre-existing tasks in the store**. They must NEVER reference the parent task being split \u2014 the parent is deleted after the split completes, and a dependency on a deleted task permanently blocks the dependent. If a child "needs the rest of the parent's work to finish first", create another sibling subtask for that remaining work and depend on the sibling. The \`task_create\` tool rejects parent-id dependencies.
51666
+ **Subtask dependencies rule:** \`dependencies\` on a child may only reference **sibling subtasks created earlier in this same split** or **pre-existing tasks in the store**. They must NEVER reference the parent task being split \u2014 the parent is deleted after the split completes, and a dependency on a deleted task permanently blocks the dependent. If a child "needs the rest of the parent's work to finish first", create another sibling subtask for that remaining work and depend on the sibling. The \`fn_task_create\` tool rejects parent-id dependencies.
51189
51667
 
51190
51668
  **Important:** If you create subtasks, this parent task will be closed and replaced by the children. Make sure each child is a complete, executable task.`;
51191
51669
  } else {
@@ -51211,7 +51689,7 @@ The user did not explicitly request subtask breakdown, so you should first asses
51211
51689
  - Adding a small feature to one module with 5 steps
51212
51690
 
51213
51691
  **How to decide:**
51214
- - If you choose to split: use the \\\`task_create\\\` tool to create the child tasks, set dependencies where needed, and then stop without writing a PROMPT.md for the parent task.
51692
+ - If you choose to split: use the \\\`fn_task_create\\\` tool to create the child tasks, set dependencies where needed, and then stop without writing a PROMPT.md for the parent task.
51215
51693
  - **Subtask dependencies must only reference sibling subtasks created earlier in this same split, or pre-existing tasks. NEVER depend on the parent task being split \u2014 the parent is deleted after splitting, and the tool will reject parent-id dependencies.**
51216
51694
  - If the work appears to be Size S, or if an M/L task genuinely has 5 or fewer focused steps with a clear scope, proceed with a normal PROMPT.md specification.
51217
51695
  - If size is uncertain at first, make a quick assessment from the available context before deciding.`;
@@ -51325,8 +51803,8 @@ Follow this structure exactly:
51325
51803
  ### Step {N}: Documentation & Delivery
51326
51804
 
51327
51805
  - [ ] Update relevant documentation
51328
- - [ ] Save documentation deliverables as task documents via \`task_document_write\` (key="docs", content=...)
51329
- - [ ] Out-of-scope findings created as new tasks via \`task_create\` tool
51806
+ - [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...)
51807
+ - [ ] Out-of-scope findings created as new tasks via \`fn_task_create\` tool
51330
51808
 
51331
51809
  ## Documentation Requirements
51332
51810
 
@@ -51359,7 +51837,7 @@ Commits at step boundaries. All commits include the task ID:
51359
51837
  - Refuse necessary fixes just because they touch files outside the initial File Scope
51360
51838
  - Commit without the task ID prefix
51361
51839
  - Remove, delete, or gut modules, settings, interfaces, exports, or test files outside the File Scope
51362
- - Remove features as "cleanup" \u2014 if something seems unused, create a task via \`task_create\`
51840
+ - Remove features as "cleanup" \u2014 if something seems unused, create a task via \`fn_task_create\`
51363
51841
 
51364
51842
  ## Changeset Requirements
51365
51843
 
@@ -51381,13 +51859,13 @@ tests. Manual verification is NOT a test.
51381
51859
  as part of this task (not just skipping tests)
51382
51860
 
51383
51861
  ## Duplicate check
51384
- Before writing a spec, call \`task_list\` to see existing tasks.
51862
+ Before writing a spec, call \`fn_task_list\` to see existing tasks.
51385
51863
  If a task already covers the same work (even if worded differently), do NOT
51386
51864
  write a PROMPT.md. Instead, write a single line to the output file:
51387
51865
  \`DUPLICATE: {existing-task-id}\`
51388
51866
 
51389
51867
  ## Dependency awareness
51390
- When you plan to list a task in the \`## Dependencies\` section, first call \`task_get\` on that task ID to read its PROMPT.md.
51868
+ When you plan to list a task in the \`## Dependencies\` section, first call \`fn_task_get\` on that task ID to read its PROMPT.md.
51391
51869
  Use what you learn \u2014 file scope, APIs, patterns, completion criteria \u2014 to make the new spec accurate: reference the right paths, avoid conflicting assumptions, and describe what the dependency must deliver before this task starts.
51392
51870
  If the dependency task has no PROMPT.md yet (not yet specified), note that in the Dependencies section.
51393
51871
 
@@ -51395,8 +51873,8 @@ If the dependency task has no PROMPT.md yet (not yet specified), note that in th
51395
51873
  When the task includes \`breakIntoSubtasks: true\`, first decide whether it should be split.
51396
51874
 
51397
51875
  - Split only when the work is meaningfully decomposable into 2-5 independently executable child tasks.
51398
- - If splitting: use the \`task_create\` tool to create child tasks in triage, include clear descriptions and dependencies between them, then stop. Do NOT write a PROMPT.md for the parent task.
51399
- - **CRITICAL \u2014 subtask dependencies:** the parent task is deleted once all subtasks are created. \`dependencies\` on a new subtask may ONLY reference sibling subtasks you have created earlier in this same split (or unrelated existing tasks). **Never depend on the parent task's id.** If a child conceptually "waits for the parent's remaining work", create a sibling subtask that does that work and depend on the sibling instead. The \`task_create\` tool will reject parent-id dependencies with an error.
51876
+ - If splitting: use the \`fn_task_create\` tool to create child tasks in triage, include clear descriptions and dependencies between them, then stop. Do NOT write a PROMPT.md for the parent task.
51877
+ - **CRITICAL \u2014 subtask dependencies:** the parent task is deleted once all subtasks are created. \`dependencies\` on a new subtask may ONLY reference sibling subtasks you have created earlier in this same split (or unrelated existing tasks). **Never depend on the parent task's id.** If a child conceptually "waits for the parent's remaining work", create a sibling subtask that does that work and depend on the sibling instead. The \`fn_task_create\` tool will reject parent-id dependencies with an error.
51400
51878
  - If not splitting: proceed with a normal PROMPT.md specification.
51401
51879
 
51402
51880
  ## Proactive Subtask Breakdown for M/L Tasks
@@ -51419,20 +51897,20 @@ For tasks you assess as Size M or L, proactively evaluate whether splitting into
51419
51897
 
51420
51898
  ## Triage tools
51421
51899
  You have these extra tools during triage:
51422
- - \`task_list\` \u2014 list existing active tasks
51423
- - \`task_get\` \u2014 inspect a task and its PROMPT.md
51424
- - \`task_create\` \u2014 create a child/follow-up task while triaging
51425
- - \`task_document_write\` \u2014 save a planning document (e.g., key="plan")
51426
- - \`task_document_read\` \u2014 read back a previously saved document
51900
+ - \`fn_task_list\` \u2014 list existing active tasks
51901
+ - \`fn_task_get\` \u2014 inspect a task and its PROMPT.md
51902
+ - \`fn_task_create\` \u2014 create a child/follow-up task while triaging
51903
+ - \`fn_task_document_write\` \u2014 save a planning document (e.g., key="plan")
51904
+ - \`fn_task_document_read\` \u2014 read back a previously saved document
51427
51905
 
51428
- When the planning conversation produces a structured plan, save it as a document with \`task_document_write(key='plan', content='...')\` so the executor can reference it during implementation.
51906
+ When the planning conversation produces a structured plan, save it as a document with \`fn_task_document_write(key='plan', content='...')\` so the executor can reference it during implementation.
51429
51907
 
51430
51908
  ## Guidelines
51431
51909
  - Read the project structure and relevant source files to understand context BEFORE writing
51432
51910
  - Be specific \u2014 name actual files, functions, and patterns from the codebase
51433
51911
  - Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step)
51434
51912
  - Always include a testing step and a documentation step
51435
- - For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`task_document_write\`
51913
+ - For tasks whose primary deliverable is documentation (updating docs, writing README, API references), include an explicit step or checkbox instructing the executor to save the final documentation content via \`fn_task_document_write\`
51436
51914
  - Include a "Do NOT" section with project-appropriate guardrails
51437
51915
  - Size assessment: S (<2h), M (2-4h), L (4-8h). Split if XL (8h+)
51438
51916
  - Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
@@ -51446,16 +51924,16 @@ package.json when explicit commands are provided.
51446
51924
 
51447
51925
  ## Spec Review
51448
51926
 
51449
- After writing the PROMPT.md, call \`review_spec()\` to get an independent quality review.
51927
+ After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review.
51450
51928
 
51451
51929
  - **APPROVE** \u2192 your spec is accepted, you're done
51452
- - **REVISE** \u2192 fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`review_spec()\` again. Repeat until approved.
51930
+ - **REVISE** \u2192 fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`fn_review_spec()\` again. Repeat until approved.
51453
51931
  - **RETHINK** \u2192 your approach was fundamentally rejected. The conversation will rewind. Read the feedback carefully and take a completely different approach. Do NOT repeat the rejected strategy.
51454
51932
 
51455
- You MUST call \`review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
51933
+ You MUST call \`fn_review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
51456
51934
 
51457
51935
  ## Output
51458
- Write the PROMPT.md directly using the write tool, then call \`review_spec()\` for review.
51936
+ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review.
51459
51937
 
51460
51938
  ## Frontend UX Criteria Injection
51461
51939
 
@@ -51702,9 +52180,10 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
51702
52180
  this.wasEnginePaused = false;
51703
52181
  const allTasks = await this.store.listTasks({ slim: true, includeArchived: false });
51704
52182
  const now = Date.now();
51705
- const triageTasks = allTasks.filter(
52183
+ const eligibleTriageTasks = allTasks.filter(
51706
52184
  (t) => t.column === "triage" && !this.processing.has(t.id) && !t.paused && t.status !== "awaiting-approval" && t.status !== "failed" && t.status !== "stuck-killed" && !(t.nextRecoveryAt && new Date(t.nextRecoveryAt).getTime() > now)
51707
52185
  );
52186
+ const triageTasks = sortTasksByPriorityThenAgeAndId(eligibleTriageTasks);
51708
52187
  const maxTriageConcurrent = settings.maxTriageConcurrent ?? settings.maxConcurrent ?? 2;
51709
52188
  const specifying = allTasks.filter(
51710
52189
  (t) => t.column === "triage" && t.status === "specifying" && !t.paused
@@ -51730,11 +52209,11 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
51730
52209
  /**
51731
52210
  * Specify a triage task by spawning an AI agent to generate a PROMPT.md.
51732
52211
  *
51733
- * After the agent writes the PROMPT.md, it calls `review_spec()` to spawn
52212
+ * After the agent writes the PROMPT.md, it calls `fn_review_spec()` to spawn
51734
52213
  * an independent reviewer agent that evaluates the specification quality.
51735
52214
  * The review loop works as follows:
51736
52215
  * - **APPROVE**: the spec is accepted and the task moves to `todo`
51737
- * - **REVISE**: the agent revises the spec and calls `review_spec()` again.
52216
+ * - **REVISE**: the agent revises the spec and calls `fn_review_spec()` again.
51738
52217
  * If the agent finishes without getting APPROVE, the task is NOT moved to
51739
52218
  * `todo` — a post-session gate requires an explicit APPROVE verdict.
51740
52219
  * - **RETHINK**: the conversation rewinds to a pre-specification checkpoint
@@ -51938,7 +52417,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
51938
52417
  const planningFallbackModelId = settings.planningFallbackModelId;
51939
52418
  const canRetryWithPlanningFallback = specReviewVerdictRef.current !== "APPROVE" && planningFallbackProvider && planningFallbackModelId && modelDesc !== `${planningFallbackProvider}/${planningFallbackModelId}`;
51940
52419
  if (canRetryWithPlanningFallback) {
51941
- const verdictDesc = specReviewVerdictRef.current === null ? "review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
52420
+ const verdictDesc = specReviewVerdictRef.current === null ? "fn_review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
51942
52421
  const fallbackDesc = `${planningFallbackProvider}/${planningFallbackModelId}`;
51943
52422
  triageLog.warn(
51944
52423
  `${task.id} primary planning model produced no approved spec (${verdictDesc}) \u2014 retrying with fallback ${fallbackDesc}`
@@ -52001,7 +52480,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52001
52480
  }
52002
52481
  }
52003
52482
  if (specReviewVerdictRef.current !== "APPROVE") {
52004
- const verdictDesc = specReviewVerdictRef.current === null ? "review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
52483
+ const verdictDesc = specReviewVerdictRef.current === null ? "fn_review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
52005
52484
  const decision = computeRecoveryDecision({
52006
52485
  recoveryRetryCount: task.recoveryRetryCount,
52007
52486
  nextRecoveryAt: task.nextRecoveryAt
@@ -52086,7 +52565,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52086
52565
  await retryableWork();
52087
52566
  }
52088
52567
  } catch (err) {
52089
- const errorMessage = err instanceof Error ? err.message : String(err);
52568
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
52090
52569
  if (err.code === "ENOENT") {
52091
52570
  triageLog.log(`${task.id} no longer exists \u2014 skipping`);
52092
52571
  } else if (this.pauseAborted.has(task.id)) {
@@ -52159,7 +52638,13 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52159
52638
  const msg = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
52160
52639
  triageLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' after specification error: ${msg}`);
52161
52640
  });
52162
- triageLog.error(`\u2717 ${task.id} specification failed:`, errorMessage);
52641
+ triageLog.error(`\u2717 ${task.id} specification failed:`, errorDetail);
52642
+ if (errorStack) {
52643
+ await this.store.logEntry(task.id, `Specification failed: ${errorMessage}`, errorStack).catch((logErr) => {
52644
+ const msg = logErr instanceof Error ? logErr.message : String(logErr);
52645
+ triageLog.warn(`${task.id}: failed to persist specification-failure stack trace: ${msg}`);
52646
+ });
52647
+ }
52163
52648
  this.options.onSpecifyError?.(task, err instanceof Error ? err : new Error(errorMessage));
52164
52649
  }
52165
52650
  } finally {
@@ -52180,7 +52665,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52180
52665
  )
52181
52666
  });
52182
52667
  const taskList = {
52183
- name: "task_list",
52668
+ name: "fn_task_list",
52184
52669
  label: "List Tasks",
52185
52670
  description: "List all tasks that aren't done. Returns ID, description, column, and dependencies for each. Use to check for duplicates before specifying.",
52186
52671
  parameters: Type2.Object({}),
@@ -52205,7 +52690,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52205
52690
  }
52206
52691
  };
52207
52692
  const taskGet = {
52208
- name: "task_get",
52693
+ name: "fn_task_get",
52209
52694
  label: "Get Task",
52210
52695
  description: "Get full details of a specific task including its PROMPT.md content. Use to verify duplicates and to read dependency task specs before writing a new PROMPT.md.",
52211
52696
  parameters: taskGetParams,
@@ -52227,7 +52712,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52227
52712
  };
52228
52713
  } catch (err) {
52229
52714
  const msg = err instanceof Error ? err.message : String(err);
52230
- triageLog.warn(`${options.parentTaskId}: task_get lookup failed for ${params.id}: ${msg}`);
52715
+ triageLog.warn(`${options.parentTaskId}: fn_task_get lookup failed for ${params.id}: ${msg}`);
52231
52716
  return {
52232
52717
  content: [
52233
52718
  { type: "text", text: `Task ${params.id} not found.` }
@@ -52238,7 +52723,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52238
52723
  }
52239
52724
  };
52240
52725
  const taskCreate = {
52241
- name: "task_create",
52726
+ name: "fn_task_create",
52242
52727
  label: "Create Child Task",
52243
52728
  description: "Create a child task (subtask) while breaking a larger task into smaller pieces. Use this when the work can be split into 2-5 independently executable tasks, either because the user requested subtask breakdown or because the task is oversized (8+ steps, 3+ packages, multiple independent deliverables). The created task will be a child of the current task being triaged. IMPORTANT: `dependencies` may ONLY reference other subtasks you have created in this same triage session. Never depend on the parent task \u2014 the parent is deleted after splitting, and stale dependency ids permanently block the dependent.",
52244
52729
  parameters: taskCreateParams3,
@@ -52276,10 +52761,10 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52276
52761
  content: [
52277
52762
  {
52278
52763
  type: "text",
52279
- text: `ERROR: task_create rejected. Invalid dependencies:
52764
+ text: `ERROR: fn_task_create rejected. Invalid dependencies:
52280
52765
  ${summary}
52281
52766
 
52282
- Remove or replace these ids and call task_create again.`
52767
+ Remove or replace these ids and call fn_task_create again.`
52283
52768
  }
52284
52769
  ],
52285
52770
  details: { rejectedDependencies: rejected }
@@ -52290,7 +52775,7 @@ Remove or replace these ids and call task_create again.`
52290
52775
  parentTask = await store.getTask(options.parentTaskId);
52291
52776
  } catch (err) {
52292
52777
  const msg = err instanceof Error ? err.message : String(err);
52293
- triageLog.warn(`${options.parentTaskId}: failed to load parent task for task_create inheritance: ${msg}`);
52778
+ triageLog.warn(`${options.parentTaskId}: failed to load parent task for fn_task_create inheritance: ${msg}`);
52294
52779
  parentTask = void 0;
52295
52780
  }
52296
52781
  const newTask = await store.createTask({
@@ -52331,13 +52816,13 @@ Remove or replace these ids and call task_create again.`
52331
52816
  return [taskList, taskGet, taskCreate];
52332
52817
  }
52333
52818
  /**
52334
- * Create the `review_spec` tool for the triage agent.
52819
+ * Create the `fn_review_spec` tool for the triage agent.
52335
52820
  *
52336
52821
  * Spawns an independent reviewer agent to evaluate the generated PROMPT.md.
52337
52822
  * Verdict handling:
52338
52823
  * - **APPROVE**: returns "APPROVE" — the triage agent's work is done.
52339
52824
  * - **REVISE**: returns the review feedback. The triage agent must fix the
52340
- * PROMPT.md and call `review_spec` again. A post-session gate in
52825
+ * PROMPT.md and call `fn_review_spec` again. A post-session gate in
52341
52826
  * `specifyTask()` prevents moving to `todo` if the last verdict is REVISE.
52342
52827
  * - **RETHINK**: rewinds the conversation to a pre-specification checkpoint
52343
52828
  * using `session.navigateTree()`. Returns a re-prompt instructing the agent
@@ -52348,7 +52833,7 @@ Remove or replace these ids and call task_create again.`
52348
52833
  const rootDir = this.rootDir;
52349
52834
  const options = this.options;
52350
52835
  return {
52351
- name: "review_spec",
52836
+ name: "fn_review_spec",
52352
52837
  label: "Review Specification",
52353
52838
  description: "Spawn a reviewer agent to evaluate the generated PROMPT.md specification. Returns APPROVE, REVISE, RETHINK, or UNAVAILABLE. Call after writing the PROMPT.md.",
52354
52839
  parameters: Type2.Object({}),
@@ -52366,7 +52851,7 @@ Remove or replace these ids and call task_create again.`
52366
52851
  "utf-8"
52367
52852
  ).catch((err) => {
52368
52853
  const msg = err instanceof Error ? err.message : String(err);
52369
- triageLog.warn(`${taskId}: failed to read PROMPT.md for review_spec (${promptPath}): ${msg}`);
52854
+ triageLog.warn(`${taskId}: failed to read PROMPT.md for fn_review_spec (${promptPath}): ${msg}`);
52370
52855
  return "";
52371
52856
  });
52372
52857
  if (!promptContent) {
@@ -52374,7 +52859,7 @@ Remove or replace these ids and call task_create again.`
52374
52859
  content: [
52375
52860
  {
52376
52861
  type: "text",
52377
- text: "UNAVAILABLE \u2014 PROMPT.md file not found or empty. Write the specification first, then call review_spec."
52862
+ text: "UNAVAILABLE \u2014 PROMPT.md file not found or empty. Write the specification first, then call fn_review_spec."
52378
52863
  }
52379
52864
  ],
52380
52865
  details: {}
@@ -52430,7 +52915,7 @@ Remove or replace these ids and call task_create again.`
52430
52915
  text = "APPROVE";
52431
52916
  break;
52432
52917
  case "REVISE":
52433
- text = `REVISE \u2014 fix the issues below, rewrite the PROMPT.md, and call review_spec() again.
52918
+ text = `REVISE \u2014 fix the issues below, rewrite the PROMPT.md, and call fn_review_spec() again.
52434
52919
 
52435
52920
  ${result.review}`;
52436
52921
  break;
@@ -52854,6 +53339,11 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
52854
53339
  };
52855
53340
  }
52856
53341
  if (existsSync21(join26(rootDir, "pnpm-lock.yaml"))) {
53342
+ if (existsSync21(join26(rootDir, "pnpm-workspace.yaml"))) {
53343
+ mergerLog.warn(
53344
+ `Inferred test command "pnpm test" in a pnpm workspace (${rootDir}). This runs the full monorepo suite on every merge. Consider setting an explicit scoped testCommand in project settings, e.g. \`pnpm -r --filter "...[main]" test\`.`
53345
+ );
53346
+ }
52857
53347
  return {
52858
53348
  command: "pnpm test",
52859
53349
  testSource: "inferred",
@@ -52962,6 +53452,7 @@ async function runVerificationCommand(store, rootDir, taskId, command, type) {
52962
53452
  stderr: "",
52963
53453
  success: false
52964
53454
  };
53455
+ const verificationStartedAt = Date.now();
52965
53456
  try {
52966
53457
  const { stdout, stderr } = await execAsync2(command, {
52967
53458
  cwd: rootDir,
@@ -52973,37 +53464,46 @@ async function runVerificationCommand(store, rootDir, taskId, command, type) {
52973
53464
  result.stderr = stderr?.toString?.() || "";
52974
53465
  result.exitCode = 0;
52975
53466
  result.success = true;
52976
- mergerLog.log(`${taskId}: ${type} command succeeded`);
52977
- await store.logEntry(taskId, `[verification] ${type} command succeeded (exit 0)`);
53467
+ const verificationDurationMs = Date.now() - verificationStartedAt;
53468
+ mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
53469
+ await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
52978
53470
  return result;
52979
53471
  } catch (error) {
53472
+ const verificationDurationMs = Date.now() - verificationStartedAt;
52980
53473
  result.stdout = error?.stdout?.toString?.() || "";
52981
53474
  result.stderr = error?.stderr?.toString?.() || "";
52982
53475
  result.exitCode = typeof error?.status === "number" ? error.status : typeof error?.code === "number" ? error.code : null;
52983
53476
  const maxBufferExceeded = error?.code === "ENOBUFS" || error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(error?.message ?? "").includes("maxBuffer");
52984
53477
  result.success = maxBufferExceeded && result.exitCode === 0;
52985
53478
  if (result.success) {
52986
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer)`);
53479
+ mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
52987
53480
  await store.logEntry(
52988
53481
  taskId,
52989
- `[verification] ${type} command succeeded (exit 0, output exceeded buffer)`
53482
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
52990
53483
  );
52991
53484
  return result;
52992
53485
  }
52993
53486
  const output = result.stderr || result.stdout || error?.message || "Unknown error";
52994
53487
  const summary = summarizeVerificationOutput(output, type);
52995
- mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}); output captured in task log`);
53488
+ mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
52996
53489
  await store.logEntry(
52997
53490
  taskId,
52998
- `[verification] ${type} command failed (exit ${result.exitCode}):
53491
+ `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
52999
53492
  ${summary}`
53000
53493
  );
53001
53494
  }
53002
53495
  return result;
53003
53496
  }
53004
- async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, _testCommand, _buildCommand) {
53497
+ async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
53005
53498
  try {
53006
53499
  mergerLog.log(`${taskId}: spawning in-merge verification fix agent`);
53500
+ const logger2 = new AgentLogger({
53501
+ store,
53502
+ taskId,
53503
+ agent: "merger",
53504
+ onAgentText: options.onAgentText,
53505
+ onAgentTool: options.onAgentTool
53506
+ });
53007
53507
  let skillContext = void 0;
53008
53508
  if (options.agentStore) {
53009
53509
  try {
@@ -53035,12 +53535,22 @@ A merge has been applied and the verification command failed. Your job is to fix
53035
53535
  6. If you cannot fix the issue, explain why`,
53036
53536
  tools: "coding",
53037
53537
  // Agent needs read/write file access
53538
+ onText: logger2.onText,
53539
+ onThinking: logger2.onThinking,
53540
+ onToolStart: logger2.onToolStart,
53541
+ onToolEnd: logger2.onToolEnd,
53038
53542
  defaultProvider: settings.defaultProvider,
53039
53543
  defaultModelId: settings.defaultModelId,
53040
53544
  defaultThinkingLevel: settings.defaultThinkingLevel,
53041
53545
  // Skill selection: use assigned agent skills if available, otherwise role fallback
53042
53546
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
53043
53547
  });
53548
+ const runId = mergeRunContext?.runId;
53549
+ const agentId = mergeRunContext?.agentId ?? "merger";
53550
+ await store.logEntry(
53551
+ taskId,
53552
+ `In-merge verification fix agent started (model: ${describeModel(session)}, runId: ${runId ?? "unknown"}, agentId: ${agentId})`
53553
+ );
53044
53554
  try {
53045
53555
  const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${taskId}.
53046
53556
 
@@ -53065,6 +53575,10 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
53065
53575
  mergerLog.warn(`\u23F3 ${taskId} in-merge fix rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
53066
53576
  }
53067
53577
  });
53578
+ await store.logEntry(
53579
+ taskId,
53580
+ `Re-running deterministic merge verification (attempt ${fixAttemptNumber ?? "unknown"})`
53581
+ );
53068
53582
  const reRunResult = await runVerificationCommand(
53069
53583
  store,
53070
53584
  rootDir,
@@ -53074,6 +53588,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
53074
53588
  );
53075
53589
  return reRunResult.success;
53076
53590
  } finally {
53591
+ await logger2.flush();
53077
53592
  await session.dispose();
53078
53593
  }
53079
53594
  } catch (err) {
@@ -53365,7 +53880,7 @@ and the bash tool returned exit code 0.
53365
53880
  1. Run the build command (shown in the prompt context below)
53366
53881
  2. If the build succeeds (exit code 0), proceed with the commit
53367
53882
  3. If the build fails (non-zero exit code), DO NOT commit. Instead:
53368
- - Call the \`report_build_failure\` tool with the real error details
53883
+ - Call the \`fn_report_build_failure\` tool with the real error details
53369
53884
  - Stop immediately and do not run \`git commit\`
53370
53885
  - Do not claim success in plain text
53371
53886
 
@@ -53404,7 +53919,7 @@ and the bash tool returned exit code 0.
53404
53919
  1. Run the build command (shown in the prompt context below)
53405
53920
  2. If the build succeeds (exit code 0), proceed with the commit
53406
53921
  3. If the build fails (non-zero exit code), DO NOT commit. Instead:
53407
- - Call the \`report_build_failure\` tool with the real error details
53922
+ - Call the \`fn_report_build_failure\` tool with the real error details
53408
53923
  - Stop immediately and do not run \`git commit\`
53409
53924
  - Do not claim success in plain text
53410
53925
 
@@ -53947,6 +54462,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53947
54462
  if (failedResult) {
53948
54463
  let fixSuccess = false;
53949
54464
  for (let fixAttempt = 1; fixAttempt <= maxFixRetries; fixAttempt++) {
54465
+ const fixAttemptStartedAt = Date.now();
53950
54466
  mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53951
54467
  await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53952
54468
  fixSuccess = await attemptInMergeVerificationFix(
@@ -53961,16 +54477,19 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53961
54477
  },
53962
54478
  settings,
53963
54479
  options,
54480
+ { runId: mergeRunId, agentId: engineRunContext.agentId },
54481
+ fixAttempt,
53964
54482
  effectiveTestCommand,
53965
54483
  effectiveBuildCommand
53966
54484
  );
54485
+ const fixAttemptDurationMs = Date.now() - fixAttemptStartedAt;
53967
54486
  if (fixSuccess) {
53968
- mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt}`);
53969
- await store.logEntry(taskId, `In-merge verification fix succeeded \u2014 verification now passes`);
54487
+ mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
54488
+ await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms \u2014 verification now passes`);
53970
54489
  break;
53971
54490
  }
53972
- mergerLog.warn(`${taskId}: in-merge verification fix attempt ${fixAttempt} \u2014 verification still fails`);
53973
- await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt} \u2014 verification still fails`);
54491
+ mergerLog.warn(`${taskId}: in-merge verification fix attempt ${fixAttempt} \u2014 verification still fails (${fixAttemptDurationMs}ms)`);
54492
+ await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} \u2014 verification still fails (${fixAttemptDurationMs}ms)`);
53974
54493
  }
53975
54494
  if (fixSuccess) {
53976
54495
  const authorArg = getCommitAuthorArg(settings);
@@ -53991,6 +54510,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53991
54510
  const fixType = effectiveBuildCommand ? "build" : "test";
53992
54511
  let fixSuccess = false;
53993
54512
  for (let fixAttempt = 1; fixAttempt <= maxFixRetries; fixAttempt++) {
54513
+ const fixAttemptStartedAt = Date.now();
53994
54514
  mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53995
54515
  await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53996
54516
  fixSuccess = await attemptInMergeVerificationFix(
@@ -54005,14 +54525,18 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
54005
54525
  },
54006
54526
  settings,
54007
54527
  options,
54528
+ { runId: mergeRunId, agentId: engineRunContext.agentId },
54529
+ fixAttempt,
54008
54530
  effectiveTestCommand,
54009
54531
  effectiveBuildCommand
54010
54532
  );
54533
+ const fixAttemptDurationMs = Date.now() - fixAttemptStartedAt;
54011
54534
  if (fixSuccess) {
54012
- mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt}`);
54013
- await store.logEntry(taskId, `In-merge verification fix succeeded`);
54535
+ mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
54536
+ await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
54014
54537
  break;
54015
54538
  }
54539
+ await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} \u2014 verification still fails (${fixAttemptDurationMs}ms)`);
54016
54540
  }
54017
54541
  if (fixSuccess) {
54018
54542
  const authorArg = getCommitAuthorArg(settings);
@@ -54091,8 +54615,15 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
54091
54615
  deletions = deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0;
54092
54616
  } catch {
54093
54617
  }
54618
+ const isEmptyCommit = filesChanged === 0;
54619
+ const recordedSha = isEmptyCommit ? void 0 : commitSha;
54620
+ if (isEmptyCommit) {
54621
+ mergerLog.warn(
54622
+ `${taskId}: local squash produced an empty commit (${commitSha?.slice(0, 8)}) \u2014 branch likely contained dupes of main. Skipping commitSha; recovery will backfill when real commit lands.`
54623
+ );
54624
+ }
54094
54625
  const mergeDetails = {
54095
- commitSha,
54626
+ commitSha: recordedSha,
54096
54627
  filesChanged,
54097
54628
  insertions,
54098
54629
  deletions,
@@ -54105,7 +54636,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
54105
54636
  autoResolvedCount: result.autoResolvedCount
54106
54637
  };
54107
54638
  await store.updateTask(taskId, { mergeDetails });
54108
- mergerLog.log(`${taskId}: merge details stored (commitSha: ${commitSha?.slice(0, 8)})`);
54639
+ mergerLog.log(`${taskId}: merge details stored (commitSha: ${recordedSha?.slice(0, 8) ?? "<deferred>"})`);
54109
54640
  } catch (err) {
54110
54641
  mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
54111
54642
  }
@@ -54461,7 +54992,7 @@ async function runAiAgentForCommit(params) {
54461
54992
  let buildFailed = false;
54462
54993
  let buildErrorMessage = "";
54463
54994
  const reportBuildFailureTool = {
54464
- name: "report_build_failure",
54995
+ name: "fn_report_build_failure",
54465
54996
  label: "Report Build Failure",
54466
54997
  description: "Report that the build verification failed. Use this when the build command returns a non-zero exit code. Provide the error details in the message parameter.",
54467
54998
  parameters: Type3.Object({
@@ -54678,7 +55209,7 @@ function buildMergePrompt(params) {
54678
55209
  "This command is mandatory before commit.",
54679
55210
  "Run it with the bash tool in the current worktree and inspect the actual exit code.",
54680
55211
  "Only proceed if it exits 0.",
54681
- "If it exits non-zero, call `report_build_failure` with the concrete error output and stop without committing."
55212
+ "If it exits non-zero, call `fn_report_build_failure` with the concrete error output and stop without committing."
54682
55213
  );
54683
55214
  }
54684
55215
  if (buildCommand) {
@@ -54690,7 +55221,7 @@ function buildMergePrompt(params) {
54690
55221
  "This command is mandatory before commit.",
54691
55222
  "Run it with the bash tool in the current worktree and inspect the actual exit code.",
54692
55223
  "Only commit if it exits 0.",
54693
- "If it exits non-zero, call `report_build_failure` with the concrete error output and stop without committing."
55224
+ "If it exits non-zero, call `fn_report_build_failure` with the concrete error output and stop without committing."
54694
55225
  );
54695
55226
  }
54696
55227
  return parts.join("\n");
@@ -55719,11 +56250,11 @@ function buildStepPrompt(taskDetail, stepIndex, rootDir, settings, worktreePath)
55719
56250
  if (isLastStep) {
55720
56251
  parts.push(
55721
56252
  "",
55722
- `**Document your deliverables:** When this task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), save that content as a task document using \`task_document_write(key='...', content='...')\`. Use a key that describes the deliverable (e.g., key="readme", key="api-docs"). The document persists in the task for review even after the worktree is cleaned up.`,
56253
+ `**Document your deliverables:** When this task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), save that content as a task document using \`fn_task_document_write(key='...', content='...')\`. Use a key that describes the deliverable (e.g., key="readme", key="api-docs"). The document persists in the task for review even after the worktree is cleaned up.`,
55723
56254
  ""
55724
56255
  );
55725
56256
  }
55726
- parts.push("After completing this step, commit your changes and call task_done(). Do NOT proceed to subsequent steps.");
56257
+ parts.push("After completing this step, commit your changes and call fn_task_done(). Do NOT proceed to subsequent steps.");
55727
56258
  return parts.join("\n");
55728
56259
  }
55729
56260
  function scopePromptToWorktree(prompt, rootDir, worktreePath) {
@@ -55778,7 +56309,7 @@ function buildReducedStepPrompt(taskDetail, stepIndex) {
55778
56309
  "IMPORTANT: Your previous attempt hit the context window limit.",
55779
56310
  "Do NOT repeat work that's already been done.",
55780
56311
  "Check git status and git log to see what's been committed.",
55781
- "Complete the remaining work and call task_done()."
56312
+ "Complete the remaining work and call fn_task_done()."
55782
56313
  ];
55783
56314
  return parts.join("\n").replace(/\n{3,}/g, "\n\n");
55784
56315
  }
@@ -56578,10 +57109,10 @@ ${attachmentsSection}${commandsSection}${memorySection}${progressSection}${steer
56578
57109
 
56579
57110
  ${reviewLevel === 0 ? "No reviews required. Implement directly." : ""}
56580
57111
  ${reviewLevel >= 1 ? `Before implementing each step (except Step 0 and the final step), call:
56581
- \`review_step(step=N, type="plan", step_name="...")\`` : ""}
57112
+ \`fn_review_step(step=N, type="plan", step_name="...")\`` : ""}
56582
57113
  ${reviewLevel >= 2 ? `After implementing + committing each step, call:
56583
- \`review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
56584
- ${reviewLevel >= 3 ? `After tests, also call review_step with type="code" for test review.` : ""}
57114
+ \`fn_review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
57115
+ ${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""}
56585
57116
 
56586
57117
  ## Worktree Boundaries
56587
57118
 
@@ -56590,23 +57121,24 @@ You are running in an **isolated git worktree**. This means:
56590
57121
  - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree.
56591
57122
  - **Exception \u2014 Project memory:** You MAY read and write to files under \`.fusion/memory/\` at the project root to save durable project learnings.
56592
57123
  - **Exception \u2014 Task attachments:** You MAY read files under \`.fusion/tasks/{taskId}/attachments/\` at the project root for context.
57124
+ - **Exception \u2014 Sibling task specs:** You MAY read \`.fusion/tasks/{taskId}/PROMPT.md\` and \`.fusion/tasks/{taskId}/task.json\` at the project root (read-only) to consult dependency tasks' specifications.
56593
57125
  - **Shell commands** run inside the worktree by default. Avoid using \`cd\` to navigate outside the worktree.
56594
57126
 
56595
57127
  ## Begin
56596
57128
 
56597
57129
  ${hasProgress ? `Resume from Step ${task.currentStep}. Do NOT redo completed steps.` : "Start with Step 0 (Preflight). Work through each step in order."}
56598
- Use \`task_update\` to report progress on every step transition.
56599
- Use \`task_log\` for important actions and decisions.
56600
- Use \`task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
57130
+ Use \`fn_task_update\` to report progress on every step transition.
57131
+ Use \`fn_task_log\` for important actions and decisions.
57132
+ Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
56601
57133
  Commit at step boundaries: \`git commit -m "feat(${task.id}): complete Step N \u2014 description"${authorArg}\`
56602
- When all steps are complete: call \`task_done()\`
57134
+ When all steps are complete: call \`fn_task_done()\`
56603
57135
 
56604
- If a build command is configured, run that exact command in this worktree before calling \`task_done()\`.
57136
+ If a build command is configured, run that exact command in this worktree before calling \`fn_task_done()\`.
56605
57137
  Treat a non-zero exit code as a blocking failure. Do not claim success without a real passing run.
56606
57138
  Run the configured/full test suite and fix failures even when that requires edits outside the original File Scope.
56607
- If the repo has a lint command (e.g. \`pnpm lint\`, \`npm run lint\`), run it before \`task_done()\` and fix any failures it reports.
56608
- If the repo has a typecheck command, run it before \`task_done()\` and fix any failures it reports.
56609
- Use \`task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
57139
+ If the repo has a lint command (e.g. \`pnpm lint\`, \`npm run lint\`), run it before \`fn_task_done()\` and fix any failures it reports.
57140
+ If the repo has a typecheck command, run it before \`fn_task_done()\` and fix any failures it reports.
57141
+ Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
56610
57142
  If lint is configured and failing, fix that too before completion.
56611
57143
  **CRITICAL: Resolve ALL test failures (and any lint/typecheck failures) before completing the task, even if they appear unrelated or pre-existing.** Unrelated failures left unfixed accumulate technical debt and block future integrations. Investigate and fix or suppress them \u2014 do not defer them to a separate task.`;
56612
57144
  }
@@ -56721,22 +57253,22 @@ You are working in a git worktree isolated from the main branch. Your job is to
56721
57253
  You have tools to report progress. The board updates in real-time.
56722
57254
 
56723
57255
  **Step lifecycle:**
56724
- - Before starting a step: \`task_update(step=N, status="in-progress")\`
56725
- - After completing a step: \`task_update(step=N, status="done")\`
56726
- - If skipping a step: \`task_update(step=N, status="skipped")\`
57256
+ - Before starting a step: \`fn_task_update(step=N, status="in-progress")\`
57257
+ - After completing a step: \`fn_task_update(step=N, status="done")\`
57258
+ - If skipping a step: \`fn_task_update(step=N, status="skipped")\`
56727
57259
 
56728
- **Logging important actions:** \`task_log(message="what happened")\`
57260
+ **Logging important actions:** \`fn_task_log(message="what happened")\`
56729
57261
 
56730
- **Out-of-scope work found during execution:** \`task_create(description="what needs doing")\`
57262
+ **Out-of-scope work found during execution:** \`fn_task_create(description="what needs doing")\`
56731
57263
  When creating multiple related tasks, declare dependencies between them:
56732
- \`task_create(description="load door sounds", dependencies=[])\` \u2192 returns KB-050
56733
- \`task_create(description="play sound on door open/close", dependencies=["KB-050"])\`
57264
+ \`fn_task_create(description="load door sounds", dependencies=[])\` \u2192 returns KB-050
57265
+ \`fn_task_create(description="play sound on door open/close", dependencies=["KB-050"])\`
56734
57266
 
56735
- **Discovered a dependency:** \`task_add_dep(task_id="KB-XXX")\` \u2014 use when you discover mid-execution that another task must be completed first. This will return a warning first \u2014 you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
57267
+ **Discovered a dependency:** \`fn_task_add_dep(task_id="KB-XXX")\` \u2014 use when you discover mid-execution that another task must be completed first. This will return a warning first \u2014 you must call again with \`confirm=true\` to proceed. Adding a dependency stops execution, discards current work, and moves the task to triage for re-specification.
56736
57268
 
56737
- ## Cross-model review via review_step tool
57269
+ ## Cross-model review via fn_review_step tool
56738
57270
 
56739
- You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
57271
+ You have a \`fn_review_step\` tool. It spawns a SEPARATE reviewer agent (different
56740
57272
  model, read-only access) to independently assess your work.
56741
57273
 
56742
57274
  **When to call it** \u2014 based on the Review Level in the PROMPT.md:
@@ -56744,8 +57276,8 @@ model, read-only access) to independently assess your work.
56744
57276
  | Review Level | Before implementing | After implementing + committing |
56745
57277
  |-------------|--------------------|---------------------------------|
56746
57278
  | 0 (None) | \u2014 | \u2014 |
56747
- | 1 (Plan) | \`review_step(step, "plan", step_name)\` | \u2014 |
56748
- | 2 (Plan+Code) | \`review_step(step, "plan", step_name)\` | \`review_step(step, "code", step_name, baseline)\` |
57279
+ | 1 (Plan) | \`fn_review_step(step, "plan", step_name)\` | \u2014 |
57280
+ | 2 (Plan+Code) | \`fn_review_step(step, "plan", step_name)\` | \`fn_review_step(step, "code", step_name, baseline)\` |
56749
57281
  | 3 (Full) | plan review | code review + test review |
56750
57282
 
56751
57283
  **Skip reviews for** Step 0 (Preflight) and the final documentation/delivery step.
@@ -56754,13 +57286,13 @@ model, read-only access) to independently assess your work.
56754
57286
  1. Before starting a step, capture baseline: \`git rev-parse HEAD\`
56755
57287
  2. Implement the step
56756
57288
  3. Commit
56757
- 4. Call \`review_step\` with the baseline SHA so the reviewer sees only your changes
57289
+ 4. Call \`fn_review_step\` with the baseline SHA so the reviewer sees only your changes
56758
57290
 
56759
57291
  **Handling verdicts:**
56760
57292
  - **APPROVE** \u2192 proceed to next step
56761
57293
  - **REVISE (code review)** \u2192 **enforced**. You MUST fix the issues, commit again,
56762
- and re-run \`review_step(type="code")\` before the step can be marked done.
56763
- \`task_update(status="done")\` will be rejected until the code review passes.
57294
+ and re-run \`fn_review_step(type="code")\` before the step can be marked done.
57295
+ \`fn_task_update(status="done")\` will be rejected until the code review passes.
56764
57296
  - **REVISE (plan review)** \u2192 advisory. Incorporate the feedback at your discretion
56765
57297
  and proceed with implementation. No re-review is required.
56766
57298
  - **RETHINK (code review)** \u2192 your code changes have been reverted and conversation rewound. Read the feedback carefully and take a fundamentally different approach. Do NOT repeat the rejected strategy.
@@ -56770,13 +57302,13 @@ model, read-only access) to independently assess your work.
56770
57302
 
56771
57303
  You can save and retrieve named documents for this task. Use these to store planning notes, research findings, or any persistent data that should survive across sessions.
56772
57304
 
56773
- - **Save a document:** \`task_document_write(key="plan", content="...")\`
56774
- - **Read a document:** \`task_document_read(key="plan")\`
56775
- - **List all documents:** \`task_document_read()\` (no key)
57305
+ - **Save a document:** \`fn_task_document_write(key="plan", content="...")\`
57306
+ - **Read a document:** \`fn_task_document_read(key="plan")\`
57307
+ - **List all documents:** \`fn_task_document_read()\` (no key)
56776
57308
 
56777
57309
  Documents are versioned \u2014 each write creates a new revision. Use meaningful keys like "plan", "notes", "research", "architecture".
56778
57310
 
56779
- **IMPORTANT \u2014 Save your deliverables as documents:** When your task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), you MUST save that content as a task document using \`task_document_write\`. Use a key that describes the deliverable (e.g., key="readme", key="api-docs", key="changelog"). Do this in addition to writing the file to disk \u2014 the document persists in the task for review even after the worktree is cleaned up.
57311
+ **IMPORTANT \u2014 Save your deliverables as documents:** When your task produces written output (documentation, specifications, reports, API references, README updates, guides, or any other content), you MUST save that content as a task document using \`fn_task_document_write\`. Use a key that describes the deliverable (e.g., key="readme", key="api-docs", key="changelog"). Do this in addition to writing the file to disk \u2014 the document persists in the task for review even after the worktree is cleaned up.
56780
57312
 
56781
57313
  If the task's PROMPT.md includes a "Documentation Requirements" section listing files to update, save each updated file's final content as a task document with a matching key.
56782
57314
 
@@ -56792,6 +57324,7 @@ You are running in an **isolated git worktree**. This means:
56792
57324
  - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree \u2014 the worktree is your isolated execution environment.
56793
57325
  - **Exception \u2014 Project memory:** You MAY read and write to files under .fusion/memory/ at the project root to save durable project learnings (architecture patterns, conventions, pitfalls).
56794
57326
  - **Exception \u2014 Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task.
57327
+ - **Exception \u2014 Sibling task specs:** You MAY read .fusion/tasks/{taskId}/PROMPT.md and .fusion/tasks/{taskId}/task.json at the project root (read-only) to consult dependency tasks' specifications.
56795
57328
  - **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree.
56796
57329
 
56797
57330
  If you attempt to write to a path outside the worktree, the file tools will reject the operation with an error explaining the boundary.
@@ -56802,7 +57335,7 @@ If you attempt to write to a path outside the worktree, the file tools will reje
56802
57335
  - Read "Context to Read First" files before starting
56803
57336
  - Follow the "Do NOT" section strictly
56804
57337
  - If tests, lint, build, or typecheck fail and the fix requires touching code outside the declared File Scope, fix those failures directly and keep the repo green
56805
- - Use \`task_create\` for genuinely separate follow-up work, not for mandatory fixes required to make this task land cleanly
57338
+ - Use \`fn_task_create\` for genuinely separate follow-up work, not for mandatory fixes required to make this task land cleanly
56806
57339
  - Update documentation listed in "Must Update" and check "Check If Affected"
56807
57340
  - NEVER delete, remove, or gut modules, interfaces, settings, exports, or test files outside your File Scope
56808
57341
  - NEVER remove features as "cleanup" \u2014 if something seems unused, create a task for investigation instead
@@ -56813,14 +57346,14 @@ If you attempt to write to a path outside the worktree, the file tools will reje
56813
57346
 
56814
57347
  You can spawn child agents to handle parallel work or specialized sub-tasks:
56815
57348
 
56816
- **When to use \`spawn_agent\`:**
57349
+ **When to use \`fn_spawn_agent\`:**
56817
57350
  - Parallel work that can be divided into independent chunks
56818
57351
  - Specialized tasks requiring different expertise or tools
56819
57352
  - Delegation of sub-tasks to specialized agents
56820
57353
 
56821
57354
  **How to spawn:**
56822
57355
  \`\`\`javascript
56823
- spawn_agent({
57356
+ fn_spawn_agent({
56824
57357
  name: "researcher",
56825
57358
  role: "engineer",
56826
57359
  task: "Research best practices for authentication in React applications"
@@ -56830,7 +57363,7 @@ spawn_agent({
56830
57363
  **Child agent behavior:**
56831
57364
  - Each child runs in its own git worktree (branched from your worktree)
56832
57365
  - Children execute autonomously and report completion
56833
- - When you end (task_done), all spawned children are terminated
57366
+ - When you end (fn_task_done), all spawned children are terminated
56834
57367
  - Check AgentStore for spawned agent status
56835
57368
 
56836
57369
  **Limits:**
@@ -56840,13 +57373,13 @@ spawn_agent({
56840
57373
  ## Completion
56841
57374
  After all steps are done, lint passes, tests pass, typecheck passes, and docs are updated:
56842
57375
  \`\`\`bash
56843
- Call \`task_done()\` to signal completion.
57376
+ Call \`fn_task_done()\` to signal completion.
56844
57377
  \`\`\`
56845
57378
 
56846
57379
  If a project build command is listed in the prompt, it is a hard completion gate:
56847
- - Run the exact build command in the current worktree before \`task_done()\`
57380
+ - Run the exact build command in the current worktree before \`fn_task_done()\`
56848
57381
  - Do not claim the build passes unless you actually ran it and got exit code 0
56849
- - If the build fails, do NOT call \`task_done()\`; keep working until it passes
57382
+ - If the build fails, do NOT call \`fn_task_done()\`; keep working until it passes
56850
57383
 
56851
57384
  Lint, tests, and typecheck are also hard quality gates:
56852
57385
  - Keep fixing failures until lint, the configured/full test suite, and typecheck all pass
@@ -57104,15 +57637,15 @@ Lint, tests, and typecheck are also hard quality gates:
57104
57637
  }
57105
57638
  /**
57106
57639
  * Check whether a task's work is complete — all steps are done or skipped.
57107
- * Used to detect tasks that called task_done() but never transitioned to in-review
57108
- * (e.g., killed by stuck detector after task_done but before moveTask).
57640
+ * Used to detect tasks that called fn_task_done() but never transitioned to in-review
57641
+ * (e.g., killed by stuck detector after fn_task_done but before moveTask).
57109
57642
  */
57110
57643
  isTaskWorkComplete(task) {
57111
57644
  if (task.steps.length === 0) return false;
57112
57645
  return task.steps.every((s) => s.status === "done" || s.status === "skipped");
57113
57646
  }
57114
57647
  isNoProgressNoTaskDoneFailure(task) {
57115
- return task.status === "failed" && task.error?.includes("without calling task_done") === true && task.steps.every((step) => step.status === "pending");
57648
+ return task.status === "failed" && task.error?.includes("without calling fn_task_done") === true && task.steps.every((step) => step.status === "pending");
57116
57649
  }
57117
57650
  async clearResumeFailureState(task) {
57118
57651
  const updates = {};
@@ -57288,7 +57821,7 @@ Lint, tests, and typecheck are also hard quality gates:
57288
57821
  continue;
57289
57822
  }
57290
57823
  if (this.isNoProgressNoTaskDoneFailure(task)) {
57291
- executorLog.log(`${task.id} failed without task_done and has no step progress \u2014 leaving for self-healing requeue`);
57824
+ executorLog.log(`${task.id} failed without fn_task_done and has no step progress \u2014 leaving for self-healing requeue`);
57292
57825
  continue;
57293
57826
  }
57294
57827
  executorLog.log(`Resuming ${task.id}: ${task.title || task.description.slice(0, 60)}`);
@@ -57513,13 +58046,15 @@ Lint, tests, and typecheck are also hard quality gates:
57513
58046
  await this.store.logEntry(task.id, `Worktree created at ${worktreePath}`, void 0, this.currentRunContext);
57514
58047
  }
57515
58048
  if (settings.worktreeInitCommand) {
58049
+ const initStartedAt = Date.now();
57516
58050
  try {
57517
58051
  const initResult = await runConfiguredCommand(settings.worktreeInitCommand, worktreePath, 3e5);
57518
58052
  if (initResult.spawnError || initResult.timedOut || initResult.exitCode !== 0) {
57519
58053
  throw new Error(configuredCommandErrorMessage(initResult));
57520
58054
  }
57521
- await this.store.logEntry(task.id, "Worktree init command completed", settings.worktreeInitCommand, this.currentRunContext);
58055
+ await this.store.logEntry(task.id, `[timing] Worktree init command completed in ${Date.now() - initStartedAt}ms`, settings.worktreeInitCommand, this.currentRunContext);
57522
58056
  } catch (err) {
58057
+ await this.store.logEntry(task.id, `[timing] Worktree init command failed after ${Date.now() - initStartedAt}ms`, void 0, this.currentRunContext);
57523
58058
  const execError = err instanceof Error ? err : new Error(String(err));
57524
58059
  const message = "stderr" in execError && typeof execError.stderr === "string" ? String(execError.stderr) : execError.message;
57525
58060
  executorLog.error(`${task.id}: worktree init command failed \u2014 first test run will likely fail: ${message}`);
@@ -57534,12 +58069,13 @@ Lint, tests, and typecheck are also hard quality gates:
57534
58069
  if (settings.setupScript) {
57535
58070
  const scriptCommand = settings.scripts?.[settings.setupScript];
57536
58071
  if (scriptCommand) {
58072
+ const setupStartedAt = Date.now();
57537
58073
  try {
57538
58074
  const setupResult = await runConfiguredCommand(scriptCommand, worktreePath, 12e4);
57539
58075
  if (setupResult.spawnError || setupResult.timedOut || setupResult.exitCode !== 0) {
57540
58076
  throw new Error(configuredCommandErrorMessage(setupResult));
57541
58077
  }
57542
- await this.store.logEntry(task.id, `Setup script '${settings.setupScript}' completed`, scriptCommand, this.currentRunContext);
58078
+ await this.store.logEntry(task.id, `[timing] Setup script '${settings.setupScript}' completed in ${Date.now() - setupStartedAt}ms`, scriptCommand, this.currentRunContext);
57543
58079
  } catch (err) {
57544
58080
  const execError = err instanceof Error ? err : new Error(String(err));
57545
58081
  const message = "stderr" in execError && typeof execError.stderr === "string" ? String(execError.stderr) : execError.message;
@@ -57705,7 +58241,7 @@ Lint, tests, and typecheck are also hard quality gates:
57705
58241
  await retryableStepWork();
57706
58242
  }
57707
58243
  } catch (err) {
57708
- const errorMessage = err instanceof Error ? err.message : String(err);
58244
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
57709
58245
  if (this.depAborted.has(task.id)) {
57710
58246
  this.depAborted.delete(task.id);
57711
58247
  await this.handleDepAbortCleanup(task.id, worktreePath);
@@ -57749,7 +58285,10 @@ Lint, tests, and typecheck are also hard quality gates:
57749
58285
  stuckRequeue = null;
57750
58286
  return;
57751
58287
  }
57752
- executorLog.error(`\u2717 ${task.id} transient error retries exhausted: ${errorMessage}`);
58288
+ executorLog.error(`\u2717 ${task.id} transient error retries exhausted: ${errorDetail}`);
58289
+ if (errorStack) {
58290
+ await this.store.logEntry(task.id, `Transient error retries exhausted: ${errorMessage}`, errorStack, this.currentRunContext);
58291
+ }
57753
58292
  await this.store.updateTask(task.id, {
57754
58293
  status: "failed",
57755
58294
  error: errorMessage,
@@ -57760,8 +58299,8 @@ Lint, tests, and typecheck are also hard quality gates:
57760
58299
  executorLog.log(`\u2717 ${task.id} transient retries exhausted \u2192 in-review`);
57761
58300
  this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
57762
58301
  } else {
57763
- executorLog.error(`\u2717 ${task.id} step-session execution failed:`, errorMessage);
57764
- await this.store.logEntry(task.id, `Step-session execution failed: ${errorMessage}`, void 0, this.currentRunContext);
58302
+ executorLog.error(`\u2717 ${task.id} step-session execution failed:`, errorDetail);
58303
+ await this.store.logEntry(task.id, `Step-session execution failed: ${errorMessage}`, errorStack ?? errorDetail, this.currentRunContext);
57765
58304
  await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
57766
58305
  await this.store.moveTask(task.id, "in-review");
57767
58306
  executorLog.log(`\u2717 ${task.id} step-session execution failed \u2192 in-review`);
@@ -57811,7 +58350,7 @@ Lint, tests, and typecheck are also hard quality gates:
57811
58350
  const reflectionTools = this.options.reflectionService && settings.reflectionEnabled && assignedAgentId ? [createReflectOnPerformanceTool(this.options.reflectionService, assignedAgentId)] : [];
57812
58351
  const assignedAgent = assignedAgentId && this.options.agentStore ? await this.options.agentStore.getAgent(assignedAgentId).catch(() => null) : null;
57813
58352
  if (executionMode === "fast") {
57814
- executorLog.log(`${task.id}: fast mode \u2014 review_step tool not injected`);
58353
+ executorLog.log(`${task.id}: fast mode \u2014 fn_review_step tool not injected`);
57815
58354
  }
57816
58355
  const customTools = [
57817
58356
  this.createTaskUpdateTool(task.id, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector),
@@ -57821,7 +58360,7 @@ Lint, tests, and typecheck are also hard quality gates:
57821
58360
  this.createTaskDoneTool(task.id, () => {
57822
58361
  taskDone = true;
57823
58362
  }),
57824
- // Skip review_step tool in fast mode — fast mode bypasses automated review gates
58363
+ // Skip fn_review_step tool in fast mode — fast mode bypasses automated review gates
57825
58364
  ...executionMode !== "fast" ? [
57826
58365
  this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector)
57827
58366
  ] : [],
@@ -57978,7 +58517,7 @@ Lint, tests, and typecheck are also hard quality gates:
57978
58517
  "3. Review the PROMPT.md steps to see which are still pending",
57979
58518
  "",
57980
58519
  "Take a DIFFERENT approach from what you were doing before.",
57981
- "If the current step is complete, call task_update to mark it done and move to the next step.",
58520
+ "If the current step is complete, call fn_task_update to mark it done and move to the next step.",
57982
58521
  "If you're stuck on a problem, try a simpler or alternative solution.",
57983
58522
  "",
57984
58523
  "Continue the task from where you left off."
@@ -58016,8 +58555,8 @@ Lint, tests, and typecheck are also hard quality gates:
58016
58555
  const implicitCheck = await this.store.getTask(task.id);
58017
58556
  if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
58018
58557
  taskDone = true;
58019
- executorLog.log(`${task.id} all steps done \u2014 treating as implicit task_done`);
58020
- await this.store.logEntry(task.id, "All steps complete \u2014 implicit task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
58558
+ executorLog.log(`${task.id} all steps done \u2014 treating as implicit fn_task_done`);
58559
+ await this.store.logEntry(task.id, "All steps complete \u2014 implicit fn_task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
58021
58560
  }
58022
58561
  }
58023
58562
  if (taskDone) {
@@ -58054,11 +58593,11 @@ Lint, tests, and typecheck are also hard quality gates:
58054
58593
  while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
58055
58594
  taskDoneSessionRetries++;
58056
58595
  executorLog.log(
58057
- `\u26A0 ${task.id} finished without task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
58596
+ `\u26A0 ${task.id} finished without fn_task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
58058
58597
  );
58059
58598
  await this.store.logEntry(
58060
58599
  task.id,
58061
- `Agent finished without calling task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
58600
+ `Agent finished without calling fn_task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
58062
58601
  void 0,
58063
58602
  this.currentRunContext
58064
58603
  );
@@ -58100,10 +58639,10 @@ Lint, tests, and typecheck are also hard quality gates:
58100
58639
  });
58101
58640
  stuckDetector?.trackTask(task.id, retrySession);
58102
58641
  const retryPrompt = [
58103
- "Your previous session ended without calling the task_done tool.",
58642
+ "Your previous session ended without calling the fn_task_done tool.",
58104
58643
  "The task may already be complete \u2014 review the current state of the worktree and either:",
58105
- "1. If the work is done, call task_done with a summary of what was accomplished.",
58106
- "2. If there is remaining work, finish it and then call task_done.",
58644
+ "1. If the work is done, call fn_task_done with a summary of what was accomplished.",
58645
+ "2. If there is remaining work, finish it and then call fn_task_done.",
58107
58646
  "",
58108
58647
  "Original task:",
58109
58648
  buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
@@ -58115,8 +58654,8 @@ Lint, tests, and typecheck are also hard quality gates:
58115
58654
  const implicitCheck = await this.store.getTask(task.id);
58116
58655
  if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
58117
58656
  taskDone = true;
58118
- executorLog.log(`${task.id} all steps done \u2014 treating as implicit task_done`);
58119
- await this.store.logEntry(task.id, "All steps complete \u2014 implicit task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
58657
+ executorLog.log(`${task.id} all steps done \u2014 treating as implicit fn_task_done`);
58658
+ await this.store.logEntry(task.id, "All steps complete \u2014 implicit fn_task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
58120
58659
  }
58121
58660
  }
58122
58661
  }
@@ -58148,7 +58687,7 @@ Lint, tests, and typecheck are also hard quality gates:
58148
58687
  } else {
58149
58688
  const priorRequeues = task.taskDoneRetryCount ?? 0;
58150
58689
  const nextRequeueCount = priorRequeues + 1;
58151
- const errorMessage = `Agent finished without calling task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
58690
+ const errorMessage = `Agent finished without calling fn_task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
58152
58691
  if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
58153
58692
  await this.store.updateTask(task.id, {
58154
58693
  status: "failed",
@@ -58167,7 +58706,7 @@ Lint, tests, and typecheck are also hard quality gates:
58167
58706
  await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
58168
58707
  await this.store.logEntry(task.id, `${errorMessage} \u2014 moved to in-review for inspection`, void 0, this.currentRunContext);
58169
58708
  await this.store.moveTask(task.id, "in-review");
58170
- executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 no task_done \u2192 in-review`);
58709
+ executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 no fn_task_done \u2192 in-review`);
58171
58710
  }
58172
58711
  this.options.onError?.(task, new Error(errorMessage));
58173
58712
  }
@@ -58203,7 +58742,7 @@ Lint, tests, and typecheck are also hard quality gates:
58203
58742
  await retryableWork();
58204
58743
  }
58205
58744
  } catch (err) {
58206
- const errorMessage = err instanceof Error ? err.message : String(err);
58745
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
58207
58746
  if (this.depAborted.has(task.id)) {
58208
58747
  this.depAborted.delete(task.id);
58209
58748
  await this.handleDepAbortCleanup(task.id, worktreePath);
@@ -58271,7 +58810,7 @@ Lint, tests, and typecheck are also hard quality gates:
58271
58810
  "2. Identify the most critical remaining work",
58272
58811
  "3. Complete it with a simpler, more focused approach",
58273
58812
  "",
58274
- "Do not repeat what's already been done. Just complete the task and call task_done."
58813
+ "Do not repeat what's already been done. Just complete the task and call fn_task_done."
58275
58814
  ].join("\n");
58276
58815
  await promptWithFallback(activeEntry.session, reducedPrompt);
58277
58816
  checkSessionError(activeEntry.session);
@@ -58346,8 +58885,8 @@ Lint, tests, and typecheck are also hard quality gates:
58346
58885
  await this.store.moveTask(task.id, "todo");
58347
58886
  return;
58348
58887
  }
58349
- executorLog.error(`\u2717 ${task.id} transient error retries exhausted (${MAX_RECOVERY_RETRIES} attempts): ${errorMessage}`);
58350
- await this.store.logEntry(task.id, `Transient error retries exhausted after ${MAX_RECOVERY_RETRIES} attempts: ${errorMessage}`, void 0, this.currentRunContext);
58888
+ executorLog.error(`\u2717 ${task.id} transient error retries exhausted (${MAX_RECOVERY_RETRIES} attempts): ${errorDetail}`);
58889
+ await this.store.logEntry(task.id, `Transient error retries exhausted after ${MAX_RECOVERY_RETRIES} attempts: ${errorMessage}`, errorStack ?? errorDetail, this.currentRunContext);
58351
58890
  await this.store.updateTask(task.id, {
58352
58891
  status: "failed",
58353
58892
  error: errorMessage,
@@ -58359,8 +58898,8 @@ Lint, tests, and typecheck are also hard quality gates:
58359
58898
  this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
58360
58899
  return;
58361
58900
  }
58362
- executorLog.error(`\u2717 ${task.id} execution failed:`, errorMessage);
58363
- await this.store.logEntry(task.id, `Execution failed: ${errorMessage}`, void 0, this.currentRunContext);
58901
+ executorLog.error(`\u2717 ${task.id} execution failed:`, errorDetail);
58902
+ await this.store.logEntry(task.id, `Execution failed: ${errorMessage}`, errorStack ?? errorDetail, this.currentRunContext);
58364
58903
  await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
58365
58904
  await this.store.moveTask(task.id, "in-review");
58366
58905
  executorLog.log(`\u2717 ${task.id} execution failed \u2192 in-review`);
@@ -58408,7 +58947,7 @@ Lint, tests, and typecheck are also hard quality gates:
58408
58947
  createTaskUpdateTool(taskId, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector) {
58409
58948
  const store = this.store;
58410
58949
  return {
58411
- name: "task_update",
58950
+ name: "fn_task_update",
58412
58951
  label: "Update Step",
58413
58952
  description: "Update a step's status. Call before starting a step (in-progress), after completing it (done), or to skip it (skipped). The board updates in real-time.",
58414
58953
  parameters: taskUpdateParams,
@@ -58421,7 +58960,7 @@ Lint, tests, and typecheck are also hard quality gates:
58421
58960
  return {
58422
58961
  content: [{
58423
58962
  type: "text",
58424
- text: `Cannot mark Step ${step} as done \u2014 the last code review returned REVISE. Fix the issues from the code review, commit your changes, and call review_step(step=${step}, type="code") again. The step can only advance after the code review passes.`
58963
+ text: `Cannot mark Step ${step} as done \u2014 the last code review returned REVISE. Fix the issues from the code review, commit your changes, and call fn_review_step(step=${step}, type="code") again. The step can only advance after the code review passes.`
58425
58964
  }],
58426
58965
  details: {}
58427
58966
  };
@@ -58460,7 +58999,7 @@ Lint, tests, and typecheck are also hard quality gates:
58460
58999
  createTaskAddDepTool(taskId) {
58461
59000
  const store = this.store;
58462
59001
  return {
58463
- name: "task_add_dep",
59002
+ name: "fn_task_add_dep",
58464
59003
  label: "Add Dependency",
58465
59004
  description: "Declare a dependency on an existing task. Use when you discover mid-execution that another task must be completed first. Adding a dependency to an in-progress task will stop execution and discard current work, so confirm=true is required. Without confirm=true, a warning is returned first.",
58466
59005
  parameters: taskAddDepParams,
@@ -58530,7 +59069,7 @@ Lint, tests, and typecheck are also hard quality gates:
58530
59069
  createTaskDoneTool(taskId, onDone) {
58531
59070
  const store = this.store;
58532
59071
  return {
58533
- name: "task_done",
59072
+ name: "fn_task_done",
58534
59073
  label: "Mark Task Done",
58535
59074
  description: "Signal that all steps are complete, tests pass, and documentation is updated. Call this as the final action after finishing all work. Automatically marks all remaining steps as done. Optionally provide a summary of what was changed/fixed.",
58536
59075
  parameters: Type4.Object({
@@ -58545,7 +59084,7 @@ Lint, tests, and typecheck are also hard quality gates:
58545
59084
  return {
58546
59085
  content: [{
58547
59086
  type: "text",
58548
- text: `Cannot mark task done yet \u2014 ${completionBlocker}. Resolve the blocker before calling task_done().`
59087
+ text: `Cannot mark task done yet \u2014 ${completionBlocker}. Resolve the blocker before calling fn_task_done().`
58549
59088
  }],
58550
59089
  details: {}
58551
59090
  };
@@ -58569,7 +59108,7 @@ Lint, tests, and typecheck are also hard quality gates:
58569
59108
  };
58570
59109
  }
58571
59110
  /**
58572
- * Create the review_step tool for the executor agent.
59111
+ * Create the fn_review_step tool for the executor agent.
58573
59112
  *
58574
59113
  * When the reviewer returns a RETHINK verdict, this tool:
58575
59114
  * 1. Runs `git reset --hard <baseline>` to revert file changes
@@ -58581,7 +59120,7 @@ Lint, tests, and typecheck are also hard quality gates:
58581
59120
  const store = this.store;
58582
59121
  const options = this.options;
58583
59122
  return {
58584
- name: "review_step",
59123
+ name: "fn_review_step",
58585
59124
  label: "Review Step",
58586
59125
  description: "Spawn a reviewer agent to evaluate your plan or code for a step. Returns APPROVE, REVISE, RETHINK, or UNAVAILABLE. Call at step boundaries based on the task's review level. Skip reviews for Step 0 (Preflight) and the final documentation step.",
58587
59126
  parameters: reviewStepParams,
@@ -58651,7 +59190,7 @@ Lint, tests, and typecheck are also hard quality gates:
58651
59190
  if (reviewType === "code") {
58652
59191
  text = `REVISE \u2014 this step cannot be marked done until the code review passes.
58653
59192
 
58654
- Fix the issues below, commit your changes, and call review_step(step=${step}, type="code", step_name="${step_name}", baseline="<new SHA>") again.
59193
+ Fix the issues below, commit your changes, and call fn_review_step(step=${step}, type="code", step_name="${step_name}", baseline="<new SHA>") again.
58655
59194
 
58656
59195
  ${result.review}`;
58657
59196
  } else {
@@ -58851,7 +59390,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
58851
59390
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
58852
59391
  return;
58853
59392
  }
58854
- const scopeLine = "All prior steps remain **done**. Apply the feedback above as an in-place fix (make the necessary code changes, commit, and call `task_done()` when complete). Do **not** re-run or re-plan any earlier step unless the feedback explicitly calls it out.";
59393
+ const scopeLine = "All prior steps remain **done**. Apply the feedback above as an in-place fix (make the necessary code changes, commit, and call `fn_task_done()` when complete). Do **not** re-run or re-plan any earlier step unless the feedback explicitly calls it out.";
58855
59394
  const revisionSectionHeader = "## Workflow Revision Instructions";
58856
59395
  const revisionSectionContent = `${revisionSectionHeader}
58857
59396
 
@@ -59150,6 +59689,7 @@ ${failureFeedback}
59150
59689
  await this.store.logEntry(task.id, `[pre-merge] Starting workflow step: ${ws.name} (${stepMode} mode)`);
59151
59690
  executorLog.log(`${task.id} \u2014 [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
59152
59691
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
59692
+ const stepStartedAtMs = Date.now();
59153
59693
  results.push({
59154
59694
  workflowStepId: ws.id,
59155
59695
  workflowStepName: ws.name,
@@ -59162,6 +59702,7 @@ ${failureFeedback}
59162
59702
  const result = stepMode === "script" ? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings) : await this.executeWorkflowStep(task, ws, worktreePath, settings);
59163
59703
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
59164
59704
  if (result.success) {
59705
+ await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
59165
59706
  await this.store.logEntry(task.id, `[pre-merge] Workflow step completed: ${ws.name}`);
59166
59707
  executorLog.log(`${task.id} \u2014 [pre-merge] workflow step passed: ${ws.name}`);
59167
59708
  const existingIdx = results.findIndex((r) => r.workflowStepId === ws.id);
@@ -59175,6 +59716,7 @@ ${failureFeedback}
59175
59716
  }
59176
59717
  await this.store.updateTask(task.id, { workflowStepResults: results });
59177
59718
  } else if (result.revisionRequested) {
59719
+ await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' requested revision after ${Date.now() - stepStartedAtMs}ms`);
59178
59720
  await this.store.logEntry(
59179
59721
  task.id,
59180
59722
  `[pre-merge] Workflow step requested revision: ${ws.name}`,
@@ -59198,6 +59740,7 @@ ${failureFeedback}
59198
59740
  stepName: ws.name
59199
59741
  };
59200
59742
  } else {
59743
+ await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' failed after ${Date.now() - stepStartedAtMs}ms`);
59201
59744
  await this.store.logEntry(
59202
59745
  task.id,
59203
59746
  `[pre-merge] Workflow step failed: ${ws.name}`,
@@ -59222,14 +59765,14 @@ ${failureFeedback}
59222
59765
  };
59223
59766
  }
59224
59767
  } catch (err) {
59225
- const errorMessage = err instanceof Error ? err.message : String(err);
59768
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
59226
59769
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
59227
59770
  await this.store.logEntry(
59228
59771
  task.id,
59229
59772
  `[pre-merge] Workflow step failed: ${ws.name}`,
59230
- errorMessage
59773
+ errorStack ?? errorDetail
59231
59774
  );
59232
- executorLog.error(`${task.id} \u2014 [pre-merge] workflow step error: ${ws.name} \u2014 ${errorMessage}`);
59775
+ executorLog.error(`${task.id} \u2014 [pre-merge] workflow step error: ${ws.name} \u2014 ${errorDetail}`);
59233
59776
  const existingIdx = results.findIndex((r) => r.workflowStepId === ws.id);
59234
59777
  if (existingIdx >= 0) {
59235
59778
  results[existingIdx] = {
@@ -59855,7 +60398,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
59855
60398
  /**
59856
60399
  * When the engine restarts mid-step, an `in-progress` step may have already
59857
60400
  * passed its code review (log: `code review Step N: APPROVE`) but not yet
59858
- * been flipped to `done` by the agent's next `task_update` call. Without
60401
+ * been flipped to `done` by the agent's next `fn_task_update` call. Without
59859
60402
  * intervention, the next executor pass re-enters the step and replays plan
59860
60403
  * + code review, which we've measured at 5–20 min of pure waste per restart.
59861
60404
  *
@@ -60118,14 +60661,14 @@ Review the work done in this worktree and evaluate it against the criteria in yo
60118
60661
  }
60119
60662
  }
60120
60663
  /**
60121
- * Create the spawn_agent tool definition.
60664
+ * Create the fn_spawn_agent tool definition.
60122
60665
  * Allows the parent agent to spawn child agents with delegated tasks.
60123
60666
  */
60124
60667
  createSpawnAgentTool(taskId, worktreePath, settings) {
60125
60668
  return {
60126
- name: "spawn_agent",
60669
+ name: "fn_spawn_agent",
60127
60670
  label: "Spawn Agent",
60128
- description: "Spawn a child agent to handle parallel work or specialized sub-tasks. Each child runs in its own git worktree (branched from your worktree) and executes autonomously. When you end (task_done), all spawned children are terminated.",
60671
+ description: "Spawn a child agent to handle parallel work or specialized sub-tasks. Each child runs in its own git worktree (branched from your worktree) and executes autonomously. When you end (fn_task_done), all spawned children are terminated.",
60129
60672
  parameters: spawnAgentParams,
60130
60673
  execute: async (_id, params) => {
60131
60674
  const { name, role, task: taskPrompt } = params;
@@ -60645,6 +61188,7 @@ var init_scheduler = __esm({
60645
61188
  }
60646
61189
  }
60647
61190
  if (todo.length === 0) return;
61191
+ todo = sortTasksByPriorityThenAgeAndId(todo);
60648
61192
  const activeScopes = /* @__PURE__ */ new Map();
60649
61193
  if (settings.groupOverlappingFiles) {
60650
61194
  for (const t of inProgress) {
@@ -63190,6 +63734,13 @@ function formatTaskIdentifier(task) {
63190
63734
  const snippet = task.description.length > maxLen ? task.description.slice(0, maxLen) + "..." : task.description;
63191
63735
  return `${task.id}: ${snippet}`;
63192
63736
  }
63737
+ function resolveNtfyBaseUrl(baseUrl, fallback = DEFAULT_NTFY_BASE_URL) {
63738
+ const trimmed = baseUrl?.trim();
63739
+ if (!trimmed) {
63740
+ return fallback;
63741
+ }
63742
+ return trimmed.replace(/\/+$/, "");
63743
+ }
63193
63744
  function resolveNtfyEvents(events) {
63194
63745
  return events ? [...events] : [...DEFAULT_NTFY_EVENTS];
63195
63746
  }
@@ -63213,7 +63764,7 @@ function buildNtfyClickUrl(options) {
63213
63764
  return query ? `${normalizedHost}/?${query}` : `${normalizedHost}/`;
63214
63765
  }
63215
63766
  async function sendNtfyNotification({
63216
- ntfyBaseUrl = "https://ntfy.sh",
63767
+ ntfyBaseUrl,
63217
63768
  topic,
63218
63769
  title,
63219
63770
  message,
@@ -63230,7 +63781,8 @@ async function sendNtfyNotification({
63230
63781
  if (clickUrl) {
63231
63782
  headers.Click = clickUrl;
63232
63783
  }
63233
- const response = await fetch(`${ntfyBaseUrl}/${topic}`, {
63784
+ const resolvedBaseUrl = resolveNtfyBaseUrl(ntfyBaseUrl);
63785
+ const response = await fetch(`${resolvedBaseUrl}/${topic}`, {
63234
63786
  method: "POST",
63235
63787
  headers,
63236
63788
  body: message,
@@ -63246,11 +63798,12 @@ async function sendNtfyNotification({
63246
63798
  schedulerLog.log(`Failed to send ntfy notification: ${err}`);
63247
63799
  }
63248
63800
  }
63249
- var DEFAULT_NTFY_EVENTS, NtfyNotifier;
63801
+ var DEFAULT_NTFY_BASE_URL, DEFAULT_NTFY_EVENTS, NtfyNotifier;
63250
63802
  var init_notifier = __esm({
63251
63803
  "../engine/src/notifier.ts"() {
63252
63804
  "use strict";
63253
63805
  init_logger2();
63806
+ DEFAULT_NTFY_BASE_URL = "https://ntfy.sh";
63254
63807
  DEFAULT_NTFY_EVENTS = [
63255
63808
  "in-review",
63256
63809
  "merged",
@@ -63262,7 +63815,8 @@ var init_notifier = __esm({
63262
63815
  NtfyNotifier = class {
63263
63816
  constructor(store, options = {}) {
63264
63817
  this.store = store;
63265
- this.ntfyBaseUrl = options.ntfyBaseUrl ?? "https://ntfy.sh";
63818
+ this.defaultNtfyBaseUrl = resolveNtfyBaseUrl(options.ntfyBaseUrl);
63819
+ this.ntfyBaseUrl = this.defaultNtfyBaseUrl;
63266
63820
  this.projectId = options.projectId;
63267
63821
  }
63268
63822
  config = {
@@ -63272,6 +63826,7 @@ var init_notifier = __esm({
63272
63826
  events: [...DEFAULT_NTFY_EVENTS]
63273
63827
  };
63274
63828
  ntfyBaseUrl;
63829
+ defaultNtfyBaseUrl;
63275
63830
  projectId;
63276
63831
  notifiedEvents = /* @__PURE__ */ new Set();
63277
63832
  abortController = null;
@@ -63410,7 +63965,7 @@ var init_notifier = __esm({
63410
63965
  };
63411
63966
  handleSettingsUpdated = (data) => {
63412
63967
  const { settings, previous } = data;
63413
- if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
63968
+ if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
63414
63969
  const wasEnabled = this.config.enabled;
63415
63970
  this.loadConfig(settings);
63416
63971
  if (this.config.enabled && !wasEnabled) {
@@ -63419,6 +63974,8 @@ var init_notifier = __esm({
63419
63974
  schedulerLog.log("NtfyNotifier disabled");
63420
63975
  } else if (this.config.topic !== previous.ntfyTopic) {
63421
63976
  schedulerLog.log("NtfyNotifier topic updated");
63977
+ } else if (this.ntfyBaseUrl !== resolveNtfyBaseUrl(previous.ntfyBaseUrl)) {
63978
+ schedulerLog.log("NtfyNotifier base URL updated");
63422
63979
  } else if (this.config.dashboardHost !== previous.ntfyDashboardHost) {
63423
63980
  schedulerLog.log("NtfyNotifier dashboard host updated");
63424
63981
  } else if (JSON.stringify(this.config.events) !== JSON.stringify(previous.ntfyEvents)) {
@@ -63433,6 +63990,7 @@ var init_notifier = __esm({
63433
63990
  dashboardHost: settings.ntfyDashboardHost,
63434
63991
  events: resolveNtfyEvents(settings.ntfyEvents)
63435
63992
  };
63993
+ this.ntfyBaseUrl = resolveNtfyBaseUrl(settings.ntfyBaseUrl, this.defaultNtfyBaseUrl);
63436
63994
  }
63437
63995
  isEventEnabled(event) {
63438
63996
  return isNtfyEventEnabled(this.config.events, event);
@@ -64822,14 +65380,14 @@ var init_agent_heartbeat = __esm({
64822
65380
  Your job:
64823
65381
  1. Check your assigned task \u2014 read the description and PROMPT.md if present.
64824
65382
  2. Do ONE useful action: analyze, review, create follow-up tasks, or log findings.
64825
- 3. Use task_create to spawn follow-up work, task_log to record observations.
64826
- 4. Use task_document_write to save durable findings, plans, or research notes.
64827
- 5. Call heartbeat_done when finished with an optional summary of what was accomplished.
65383
+ 3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations.
65384
+ 4. Use fn_task_document_write to save durable findings, plans, or research notes.
65385
+ 5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
64828
65386
 
64829
65387
  Keep work lightweight \u2014 this is a single-pass check, not a full implementation run.
64830
- You have readonly file access plus task_create, task_log, and task_document tools.
65388
+ You have readonly file access plus fn_task_create, fn_task_log, and fn_task_document tools.
64831
65389
 
64832
- **Task Documents:** Save important findings with task_document_write(key="...", content="...").
65390
+ **Task Documents:** Save important findings with fn_task_document_write(key="...", content="...").
64833
65391
  Documents persist across sessions and are visible in the dashboard's Documents tab.
64834
65392
 
64835
65393
  ## Memory Boundaries
@@ -64842,12 +65400,12 @@ You may receive an Agent Memory section and a Project Memory section.
64842
65400
  ## Processing Messages
64843
65401
 
64844
65402
  When you are woken by an incoming message (source includes "wake-on-message"), you should:
64845
- 1. Use read_messages to check your inbox for unread messages.
65403
+ 1. Use fn_read_messages to check your inbox for unread messages.
64846
65404
  2. Review each message and determine the appropriate action:
64847
- - If the message requires a response, use send_message to reply.
64848
- - When replying, include 'reply_to_message_id' with the original message ID from read_messages output.
64849
- - If the message is informational, acknowledge it by logging with task_log.
64850
- - If the message requests work, create a follow-up task with task_create or handle it directly.
65405
+ - If the message requires a response, use fn_send_message to reply.
65406
+ - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output.
65407
+ - If the message is informational, acknowledge it by logging with fn_task_log.
65408
+ - If the message requests work, create a follow-up task with fn_task_create or handle it directly.
64851
65409
  3. After processing messages, continue with your normal heartbeat duties.
64852
65410
 
64853
65411
  When sending messages:
@@ -64860,17 +65418,17 @@ When sending messages:
64860
65418
  Your job:
64861
65419
  1. Review your context \u2014 check messages, memory, and project state.
64862
65420
  2. Do ONE useful action: analyze, create follow-up tasks, delegate work, or update memory.
64863
- 3. Use task_create to spawn follow-up work.
64864
- 4. Use list_agents and delegate_task to coordinate with other agents.
64865
- 5. Call heartbeat_done when finished with an optional summary of what was accomplished.
65421
+ 3. Use fn_task_create to spawn follow-up work.
65422
+ 4. Use fn_list_agents and fn_delegate_task to coordinate with other agents.
65423
+ 5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
64866
65424
 
64867
65425
  Keep work lightweight \u2014 this is a single-pass ambient check, not a full implementation run.
64868
65426
  You have readonly file access plus:
64869
- - task_create
64870
- - list_agents and delegate_task
64871
- - memory_search, memory_get, and memory_append
64872
- - heartbeat_done
64873
- - send_message and read_messages when messaging is enabled for this run (they may not always be available)
65427
+ - fn_task_create
65428
+ - fn_list_agents and fn_delegate_task
65429
+ - fn_memory_search, fn_memory_get, and fn_memory_append
65430
+ - fn_heartbeat_done
65431
+ - fn_send_message and fn_read_messages when messaging is enabled for this run (they may not always be available)
64874
65432
 
64875
65433
  ## Memory Boundaries
64876
65434
 
@@ -64882,12 +65440,12 @@ You may receive an Agent Memory section and a Project Memory section.
64882
65440
  ## Processing Messages
64883
65441
 
64884
65442
  When you are woken by an incoming message (source includes "wake-on-message"), you should:
64885
- 1. If read_messages is available, use it to check your inbox for unread messages.
65443
+ 1. If fn_read_messages is available, use it to check your inbox for unread messages.
64886
65444
  2. Review each message and determine the appropriate action:
64887
- - If the message requires a response and send_message is available, use send_message to reply.
64888
- - When replying, include 'reply_to_message_id' with the original message ID from read_messages output.
64889
- - If the message is informational, acknowledge it and respond via send_message when appropriate.
64890
- - If the message requests work, create a follow-up task with task_create.
65445
+ - If the message requires a response and fn_send_message is available, use fn_send_message to reply.
65446
+ - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output.
65447
+ - If the message is informational, acknowledge it and respond via fn_send_message when appropriate.
65448
+ - If the message requests work, create a follow-up task with fn_task_create.
64891
65449
  3. After processing messages, continue with your ambient work.
64892
65450
 
64893
65451
  When sending messages:
@@ -65294,7 +65852,7 @@ When sending messages:
65294
65852
  * Implements the Paperclip-style execution model:
65295
65853
  * 1. Wake — start a heartbeat run record
65296
65854
  * 2. Check inbox — resolve the agent's assigned task
65297
- * 3. Work — run a lightweight agent session with readonly tools + task_create/task_log
65855
+ * 3. Work — run a lightweight agent session with readonly tools + fn_task_create/fn_task_log
65298
65856
  * 4. Exit — record results and complete the run
65299
65857
  *
65300
65858
  * Budget governance:
@@ -65544,7 +66102,7 @@ When sending messages:
65544
66102
  stdoutExcerpt += delta.slice(0, remaining);
65545
66103
  };
65546
66104
  const heartbeatDoneTool = {
65547
- name: "heartbeat_done",
66105
+ name: "fn_heartbeat_done",
65548
66106
  label: "Heartbeat Done",
65549
66107
  description: "Signal that the heartbeat execution is complete. Call when finished.",
65550
66108
  parameters: heartbeatDoneParams,
@@ -65673,16 +66231,16 @@ When sending messages:
65673
66231
  "You have identity (soul, instructions, and/or memory) loaded, which means you can perform",
65674
66232
  "useful ambient work. Here are some things you can do:",
65675
66233
  "",
65676
- "1. **Check your messages** \u2014 Use read_messages to review any pending messages",
65677
- " and use send_message with reply_to_message_id when responding.",
66234
+ "1. **Check your messages** \u2014 Use fn_read_messages to review any pending messages",
66235
+ " and use fn_send_message with reply_to_message_id when responding.",
65678
66236
  "",
65679
- "2. **Create new tasks** \u2014 Use task_create to spawn follow-up work that needs",
66237
+ "2. **Create new tasks** \u2014 Use fn_task_create to spawn follow-up work that needs",
65680
66238
  " to be done. This is useful for surfacing issues or ideas you discover.",
65681
66239
  "",
65682
- "3. **Delegate work** \u2014 Use list_agents to discover available agents and",
65683
- " delegate_task to assign work to them.",
66240
+ "3. **Delegate work** \u2014 Use fn_list_agents to discover available agents and",
66241
+ " fn_delegate_task to assign work to them.",
65684
66242
  "",
65685
- "4. **Update your memory** \u2014 Use memory_append to persist important learnings",
66243
+ "4. **Update your memory** \u2014 Use fn_memory_append to persist important learnings",
65686
66244
  " or context that will help you in future sessions.",
65687
66245
  "",
65688
66246
  "5. **Monitor the project** \u2014 Review the task board and identify any issues",
@@ -65691,7 +66249,7 @@ When sending messages:
65691
66249
  "",
65692
66250
  "Your soul, instructions, and memory are already loaded in the system prompt.",
65693
66251
  "Focus on work that benefits the project without requiring a specific task context.",
65694
- "Call heartbeat_done when finished."
66252
+ "Call fn_heartbeat_done when finished."
65695
66253
  ].join("\n");
65696
66254
  } else {
65697
66255
  const taskTitle = taskDetail.title ?? taskDetail.description.slice(0, 100);
@@ -65751,7 +66309,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65751
66309
  ...triggeringCommentLines,
65752
66310
  ...pendingMessagesLines,
65753
66311
  "",
65754
- "Review the task status and take appropriate action. Call heartbeat_done when finished."
66312
+ "Review the task status and take appropriate action. Call fn_heartbeat_done when finished."
65755
66313
  ].join("\n");
65756
66314
  }
65757
66315
  await promptWithFallback2(session, executionPrompt);
@@ -65783,12 +66341,12 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65783
66341
  });
65784
66342
  heartbeatLog.log(`Heartbeat completed for ${agentId} (${toolCallCount} tool calls, ~${estimatedOutputTokens} output tokens)`);
65785
66343
  } catch (err) {
65786
- const errorMessage = err instanceof Error ? err.message : String(err);
65787
- heartbeatLog.error(`Heartbeat execution failed for ${agentId}: ${errorMessage}`);
66344
+ const errorDetail = formatError(err).detail;
66345
+ heartbeatLog.error(`Heartbeat execution failed for ${agentId}: ${errorDetail}`);
65788
66346
  await flushAgentLogger();
65789
66347
  await this.completeRun(agentId, run.id, {
65790
66348
  status: "failed",
65791
- stderrExcerpt: errorMessage,
66349
+ stderrExcerpt: errorDetail,
65792
66350
  stdoutExcerpt: stdoutExcerpt || void 0
65793
66351
  });
65794
66352
  } finally {
@@ -65807,13 +66365,14 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65807
66365
  }
65808
66366
  return await this.store.getRunDetail(agentId, run.id);
65809
66367
  } catch (err) {
66368
+ const errorDetail = formatError(err).detail;
65810
66369
  const errorMessage = err instanceof Error ? err.message : String(err);
65811
- heartbeatLog.error(`Heartbeat execution error for ${agentId}: ${errorMessage}`);
66370
+ heartbeatLog.error(`Heartbeat execution error for ${agentId}: ${errorDetail}`);
65812
66371
  await flushAgentLogger();
65813
66372
  try {
65814
66373
  await this.completeRun(agentId, run.id, {
65815
66374
  status: "failed",
65816
- stderrExcerpt: errorMessage
66375
+ stderrExcerpt: errorDetail
65817
66376
  });
65818
66377
  } catch (completeRunErr) {
65819
66378
  const completeRunErrMsg = completeRunErr instanceof Error ? completeRunErr.message : String(completeRunErr);
@@ -65851,7 +66410,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65851
66410
  *
65852
66411
  * @param agentId - The agent ID (used for tracking and logging)
65853
66412
  * @param taskStore - TaskStore for task creation and logging
65854
- * @param taskId - The assigned task ID (for task_log context)
66413
+ * @param taskId - The assigned task ID (for fn_task_log context)
65855
66414
  * @param runContext - Optional run context for mutation correlation
65856
66415
  * @param audit - Optional run auditor for audit trail (FN-1404)
65857
66416
  * @param messageStore - Optional MessageStore for messaging tools
@@ -69927,7 +70486,7 @@ var init_sse_buffer = __esm({
69927
70486
  });
69928
70487
 
69929
70488
  // ../dashboard/src/ai-session-diagnostics.ts
69930
- import { randomUUID as randomUUID10 } from "node:crypto";
70489
+ import { randomUUID as randomUUID9 } from "node:crypto";
69931
70490
  function defaultSink(level, scope, message, context) {
69932
70491
  const prefix = `[${scope}]`;
69933
70492
  const logArgs = [prefix, message, context];
@@ -69971,7 +70530,7 @@ function emit(level, scope, message, context) {
69971
70530
  const fullContext = {
69972
70531
  ...context,
69973
70532
  _emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
69974
- _diagnosticsId: randomUUID10()
70533
+ _diagnosticsId: randomUUID9()
69975
70534
  };
69976
70535
  try {
69977
70536
  _sink(level, scope, message, fullContext);
@@ -70028,7 +70587,7 @@ var init_ai_session_diagnostics = __esm({
70028
70587
  });
70029
70588
 
70030
70589
  // ../dashboard/src/planning.ts
70031
- import { randomUUID as randomUUID11 } from "node:crypto";
70590
+ import { randomUUID as randomUUID10 } from "node:crypto";
70032
70591
  import { EventEmitter as EventEmitter17 } from "node:events";
70033
70592
  async function initEngine2() {
70034
70593
  try {
@@ -70222,7 +70781,7 @@ async function createSession(ip, initialPlan, _store, rootDir, promptOverrides)
70222
70781
  if (!rootDir) {
70223
70782
  throw new Error("rootDir is required for AI-powered planning sessions");
70224
70783
  }
70225
- const sessionId = randomUUID11();
70784
+ const sessionId = randomUUID10();
70226
70785
  const session = {
70227
70786
  id: sessionId,
70228
70787
  ip,
@@ -72749,7 +73308,7 @@ var init_github_poll = __esm({
72749
73308
 
72750
73309
  // ../dashboard/src/terminal.ts
72751
73310
  import { spawn as spawn2 } from "node:child_process";
72752
- import { randomUUID as randomUUID12 } from "node:crypto";
73311
+ import { randomUUID as randomUUID11 } from "node:crypto";
72753
73312
  import { EventEmitter as EventEmitter19 } from "node:events";
72754
73313
  function extractBaseCommand(command) {
72755
73314
  let trimmed = command.trim();
@@ -72909,7 +73468,7 @@ var init_terminal = __esm({
72909
73468
  if (!validation.valid) {
72910
73469
  return { sessionId: "", error: validation.error };
72911
73470
  }
72912
- const sessionId = randomUUID12();
73471
+ const sessionId = randomUUID11();
72913
73472
  const childProcess = spawn2(command, [], {
72914
73473
  cwd,
72915
73474
  shell: true,
@@ -73281,6 +73840,8 @@ function cleanupExpiredSessions3() {
73281
73840
  diagnostics4.info("Cleanup completed", {
73282
73841
  cleanedSessions,
73283
73842
  cleanedRateLimits,
73843
+ ttlMs: SESSION_TTL_MS3,
73844
+ rateLimitWindowMs: RATE_LIMIT_WINDOW_MS3,
73284
73845
  operation: "cleanup-expired"
73285
73846
  });
73286
73847
  }
@@ -73615,7 +74176,7 @@ async function initPromptOverrides() {
73615
74176
  promptOverridesReady = true;
73616
74177
  }
73617
74178
  }
73618
- var mkdtemp, access3, stat6, mkdir12, readdir8, rm, fsReadFile, fsWriteFile, upload, execFileAsync, resolveWorkflowStepRefinePrompt, promptOverridesReady, DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
74179
+ var mkdtemp, access3, stat6, mkdir12, readdir8, rm2, fsReadFile, fsWriteFile, upload, execFileAsync, resolveWorkflowStepRefinePrompt, promptOverridesReady, DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
73619
74180
  var init_routes = __esm({
73620
74181
  "../dashboard/src/routes.ts"() {
73621
74182
  "use strict";
@@ -73652,7 +74213,7 @@ var init_routes = __esm({
73652
74213
  stat: stat6,
73653
74214
  mkdir: mkdir12,
73654
74215
  readdir: readdir8,
73655
- rm,
74216
+ rm: rm2,
73656
74217
  readFile: fsReadFile,
73657
74218
  writeFile: fsWriteFile
73658
74219
  } = fsPromises);
@@ -75739,7 +76300,7 @@ var require_extension = __commonJS({
75739
76300
  if (dest[name] === void 0) dest[name] = [elem];
75740
76301
  else dest[name].push(elem);
75741
76302
  }
75742
- function parse2(header) {
76303
+ function parse(header) {
75743
76304
  const offers = /* @__PURE__ */ Object.create(null);
75744
76305
  let params = /* @__PURE__ */ Object.create(null);
75745
76306
  let mustUnescape = false;
@@ -75879,7 +76440,7 @@ var require_extension = __commonJS({
75879
76440
  }).join(", ");
75880
76441
  }).join(", ");
75881
76442
  }
75882
- module.exports = { format, parse: parse2 };
76443
+ module.exports = { format, parse };
75883
76444
  }
75884
76445
  });
75885
76446
 
@@ -75913,7 +76474,7 @@ var require_websocket = __commonJS({
75913
76474
  var {
75914
76475
  EventTarget: { addEventListener, removeEventListener }
75915
76476
  } = require_event_target();
75916
- var { format, parse: parse2 } = require_extension();
76477
+ var { format, parse } = require_extension();
75917
76478
  var { toBuffer } = require_buffer_util();
75918
76479
  var kAborted = /* @__PURE__ */ Symbol("kAborted");
75919
76480
  var protocolVersions = [8, 13];
@@ -76582,7 +77143,7 @@ var require_websocket = __commonJS({
76582
77143
  }
76583
77144
  let extensions;
76584
77145
  try {
76585
- extensions = parse2(secWebSocketExtensions);
77146
+ extensions = parse(secWebSocketExtensions);
76586
77147
  } catch (err) {
76587
77148
  const message = "Invalid Sec-WebSocket-Extensions header";
76588
77149
  abortHandshake(websocket, socket, message);
@@ -76872,7 +77433,7 @@ var require_subprotocol = __commonJS({
76872
77433
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
76873
77434
  "use strict";
76874
77435
  var { tokenChars } = require_validation();
76875
- function parse2(header) {
77436
+ function parse(header) {
76876
77437
  const protocols = /* @__PURE__ */ new Set();
76877
77438
  let start = -1;
76878
77439
  let end = -1;
@@ -76908,7 +77469,7 @@ var require_subprotocol = __commonJS({
76908
77469
  protocols.add(protocol);
76909
77470
  return protocols;
76910
77471
  }
76911
- module.exports = { parse: parse2 };
77472
+ module.exports = { parse };
76912
77473
  }
76913
77474
  });
76914
77475
 
@@ -77478,7 +78039,7 @@ var init_auth_middleware = __esm({
77478
78039
 
77479
78040
  // ../dashboard/src/server.ts
77480
78041
  import express from "express";
77481
- import { join as join34, dirname as dirname7 } from "node:path";
78042
+ import { join as join34, dirname as dirname8 } from "node:path";
77482
78043
  import { fileURLToPath as fileURLToPath2 } from "node:url";
77483
78044
  function clearAiSessionCleanupInterval() {
77484
78045
  if (!aiSessionCleanupIntervalHandle) {
@@ -77512,7 +78073,7 @@ var init_server = __esm({
77512
78073
  init_chat();
77513
78074
  init_dev_server_routes();
77514
78075
  init_auth_middleware();
77515
- __dirname = dirname7(fileURLToPath2(import.meta.url));
78076
+ __dirname = dirname8(fileURLToPath2(import.meta.url));
77516
78077
  MIN_AI_SESSION_TTL_MS = 10 * 60 * 1e3;
77517
78078
  MAX_AI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
77518
78079
  MIN_AI_SESSION_CLEANUP_INTERVAL_MS = 60 * 1e3;
@@ -77581,7 +78142,7 @@ var init_src3 = __esm({
77581
78142
  });
77582
78143
 
77583
78144
  // src/project-context.ts
77584
- import { resolve as resolve14, dirname as dirname8 } from "node:path";
78145
+ import { resolve as resolve14, dirname as dirname9 } from "node:path";
77585
78146
  import { existsSync as existsSync28 } from "node:fs";
77586
78147
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
77587
78148
  const central = new CentralCore(globalDir);
@@ -77672,7 +78233,7 @@ async function detectProjectFromCwd(cwd, central) {
77672
78233
  path: currentDir
77673
78234
  };
77674
78235
  }
77675
- const parentDir = dirname8(currentDir);
78236
+ const parentDir = dirname9(currentDir);
77676
78237
  if (parentDir === currentDir) {
77677
78238
  break;
77678
78239
  }
@@ -77834,11 +78395,11 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
77834
78395
  console.log(` Path: .fusion/tasks/${task.id}/`);
77835
78396
  if (attachFiles && attachFiles.length > 0) {
77836
78397
  const { readFile: readFile19 } = await import("node:fs/promises");
77837
- const { basename: basename8, extname: extname2, resolve: resolve16 } = await import("node:path");
78398
+ const { basename: basename9, extname: extname3, resolve: resolve16 } = await import("node:path");
77838
78399
  for (const filePath of attachFiles) {
77839
78400
  const resolvedPath = resolve16(filePath);
77840
- const filename = basename8(resolvedPath);
77841
- const ext = extname2(filename).toLowerCase();
78401
+ const filename = basename9(resolvedPath);
78402
+ const ext = extname3(filename).toLowerCase();
77842
78403
  const mimeType = MIME_TYPES[ext];
77843
78404
  if (!mimeType) {
77844
78405
  console.error(` \u2717 Unsupported file type: ${ext} (${filename})`);
@@ -78082,11 +78643,11 @@ async function runTaskMerge(id, projectName) {
78082
78643
  }
78083
78644
  async function runTaskAttach(id, filePath, projectName) {
78084
78645
  const { readFile: readFile19 } = await import("node:fs/promises");
78085
- const { basename: basename8, extname: extname2 } = await import("node:path");
78646
+ const { basename: basename9, extname: extname3 } = await import("node:path");
78086
78647
  const { resolve: resolve16 } = await import("node:path");
78087
78648
  const resolvedPath = resolve16(filePath);
78088
- const filename = basename8(resolvedPath);
78089
- const ext = extname2(filename).toLowerCase();
78649
+ const filename = basename9(resolvedPath);
78650
+ const ext = extname3(filename).toLowerCase();
78090
78651
  const mimeType = MIME_TYPES[ext];
78091
78652
  if (!mimeType) {
78092
78653
  console.error(`Unsupported file type: ${ext}`);
@@ -79037,7 +79598,7 @@ init_src();
79037
79598
  init_gh_cli();
79038
79599
  import { Type as Type7 } from "typebox";
79039
79600
  import { StringEnum } from "@mariozechner/pi-ai";
79040
- import { resolve as resolve15, basename as basename7, extname, join as join36 } from "node:path";
79601
+ import { resolve as resolve15, basename as basename8, extname as extname2, join as join36 } from "node:path";
79041
79602
  import { readFile as readFile18 } from "node:fs/promises";
79042
79603
  import { existsSync as existsSync30 } from "node:fs";
79043
79604
  import { spawn as spawn4 } from "node:child_process";
@@ -79358,8 +79919,8 @@ Column: triage
79358
79919
  }),
79359
79920
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
79360
79921
  const filePath = resolve15(ctx.cwd, params.path.replace(/^@/, ""));
79361
- const filename = basename7(filePath);
79362
- const ext = extname(filename).toLowerCase();
79922
+ const filename = basename8(filePath);
79923
+ const ext = extname2(filename).toLowerCase();
79363
79924
  const mimeType = MIME_TYPES2[ext];
79364
79925
  if (!mimeType) {
79365
79926
  throw new Error(