@runfusion/fusion 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +2 -0
  2. package/dist/bin.js +2069 -865
  3. package/dist/client/assets/index-BuenKJX0.css +1 -0
  4. package/dist/client/assets/index-CjGu8HRV.js +1250 -0
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/sw.js +45 -1
  7. package/dist/client/theme-data.css +109 -0
  8. package/dist/extension.js +797 -345
  9. package/dist/pi-claude-cli/index.ts +131 -0
  10. package/dist/pi-claude-cli/package.json +39 -0
  11. package/dist/pi-claude-cli/src/control-handler.ts +68 -0
  12. package/dist/pi-claude-cli/src/event-bridge.ts +386 -0
  13. package/dist/pi-claude-cli/src/mcp-config.ts +111 -0
  14. package/dist/pi-claude-cli/src/mcp-schema-server.cjs +49 -0
  15. package/dist/pi-claude-cli/src/process-manager.ts +218 -0
  16. package/dist/pi-claude-cli/src/prompt-builder.ts +536 -0
  17. package/dist/pi-claude-cli/src/provider.ts +354 -0
  18. package/dist/pi-claude-cli/src/stream-parser.ts +37 -0
  19. package/dist/pi-claude-cli/src/thinking-config.ts +83 -0
  20. package/dist/pi-claude-cli/src/tool-mapping.ts +147 -0
  21. package/dist/pi-claude-cli/src/types.ts +87 -0
  22. package/package.json +6 -5
  23. package/skill/fusion/SKILL.md +5 -3
  24. package/skill/fusion/references/cli-commands.md +22 -22
  25. package/skill/fusion/references/extension-tools.md +3 -1
  26. package/skill/fusion/references/fusion-capabilities.md +28 -35
  27. package/skill/fusion/references/task-structure.md +4 -4
  28. package/skill/fusion/workflows/dashboard-cli.md +6 -6
  29. package/skill/fusion/workflows/specifications.md +5 -3
  30. package/skill/fusion/workflows/task-lifecycle.md +1 -1
  31. package/skill/fusion/workflows/task-management.md +3 -1
  32. package/dist/client/assets/index-Djv5vKo0.css +0 -1
  33. package/dist/client/assets/index-zfXYuUXG.js +0 -1241
package/dist/extension.js CHANGED
@@ -767,7 +767,7 @@ function validateMessageMetadata(metadata) {
767
767
  throw new Error("metadata.replyTo.messageId must be a non-empty string");
768
768
  }
769
769
  }
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;
770
+ 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
771
  var init_types = __esm({
772
772
  "../core/src/types.ts"() {
773
773
  "use strict";
@@ -776,6 +776,8 @@ var init_types = __esm({
776
776
  init_error_message();
777
777
  THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
778
778
  COLUMNS = ["triage", "todo", "in-progress", "in-review", "done", "archived"];
779
+ TASK_PRIORITIES = ["low", "normal", "high", "urgent"];
780
+ DEFAULT_TASK_PRIORITY = "normal";
779
781
  EXECUTION_MODES = ["standard", "fast"];
780
782
  DEFAULT_EXECUTION_MODE = "standard";
781
783
  THEME_MODES = ["dark", "light", "system"];
@@ -2069,13 +2071,14 @@ var init_db = __esm({
2069
2071
  "../core/src/db.ts"() {
2070
2072
  "use strict";
2071
2073
  init_types();
2072
- SCHEMA_VERSION = 42;
2074
+ SCHEMA_VERSION = 45;
2073
2075
  SCHEMA_SQL = `
2074
2076
  -- Tasks table with JSON columns for nested data
2075
2077
  CREATE TABLE IF NOT EXISTS tasks (
2076
2078
  id TEXT PRIMARY KEY,
2077
2079
  title TEXT,
2078
2080
  description TEXT NOT NULL,
2081
+ priority TEXT DEFAULT 'normal',
2079
2082
  "column" TEXT NOT NULL,
2080
2083
  status TEXT,
2081
2084
  size TEXT,
@@ -2103,6 +2106,12 @@ CREATE TABLE IF NOT EXISTS tasks (
2103
2106
  summary TEXT,
2104
2107
  thinkingLevel TEXT,
2105
2108
  executionMode TEXT DEFAULT 'standard',
2109
+ tokenUsageInputTokens INTEGER,
2110
+ tokenUsageOutputTokens INTEGER,
2111
+ tokenUsageCachedTokens INTEGER,
2112
+ tokenUsageTotalTokens INTEGER,
2113
+ tokenUsageFirstUsedAt TEXT,
2114
+ tokenUsageLastUsedAt TEXT,
2106
2115
  createdAt TEXT NOT NULL,
2107
2116
  updatedAt TEXT NOT NULL,
2108
2117
  columnMovedAt TEXT,
@@ -2116,6 +2125,11 @@ CREATE TABLE IF NOT EXISTS tasks (
2116
2125
  workflowStepResults TEXT DEFAULT '[]',
2117
2126
  prInfo TEXT,
2118
2127
  issueInfo TEXT,
2128
+ sourceIssueProvider TEXT,
2129
+ sourceIssueRepository TEXT,
2130
+ sourceIssueExternalIssueId TEXT,
2131
+ sourceIssueNumber INTEGER,
2132
+ sourceIssueUrl TEXT,
2119
2133
  mergeDetails TEXT,
2120
2134
  breakIntoSubtasks INTEGER DEFAULT 0,
2121
2135
  enabledWorkflowSteps TEXT DEFAULT '[]',
@@ -3410,6 +3424,35 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
3410
3424
  `);
3411
3425
  });
3412
3426
  }
3427
+ if (version < 43) {
3428
+ this.applyMigration(43, () => {
3429
+ this.addColumnIfMissing("tasks", "priority", "TEXT DEFAULT 'normal'");
3430
+ this.db.exec(`
3431
+ UPDATE tasks
3432
+ SET priority = 'normal'
3433
+ WHERE priority IS NULL OR priority = '' OR priority NOT IN ('low', 'normal', 'high', 'urgent')
3434
+ `);
3435
+ });
3436
+ }
3437
+ if (version < 44) {
3438
+ this.applyMigration(44, () => {
3439
+ this.addColumnIfMissing("tasks", "tokenUsageInputTokens", "INTEGER");
3440
+ this.addColumnIfMissing("tasks", "tokenUsageOutputTokens", "INTEGER");
3441
+ this.addColumnIfMissing("tasks", "tokenUsageCachedTokens", "INTEGER");
3442
+ this.addColumnIfMissing("tasks", "tokenUsageTotalTokens", "INTEGER");
3443
+ this.addColumnIfMissing("tasks", "tokenUsageFirstUsedAt", "TEXT");
3444
+ this.addColumnIfMissing("tasks", "tokenUsageLastUsedAt", "TEXT");
3445
+ });
3446
+ }
3447
+ if (version < 45) {
3448
+ this.applyMigration(45, () => {
3449
+ this.addColumnIfMissing("tasks", "sourceIssueProvider", "TEXT");
3450
+ this.addColumnIfMissing("tasks", "sourceIssueRepository", "TEXT");
3451
+ this.addColumnIfMissing("tasks", "sourceIssueExternalIssueId", "TEXT");
3452
+ this.addColumnIfMissing("tasks", "sourceIssueNumber", "INTEGER");
3453
+ this.addColumnIfMissing("tasks", "sourceIssueUrl", "TEXT");
3454
+ });
3455
+ }
3413
3456
  }
3414
3457
  /**
3415
3458
  * Run a single migration step inside a transaction and bump the version.
@@ -5680,6 +5723,54 @@ var init_message_store = __esm({
5680
5723
  }
5681
5724
  });
5682
5725
 
5726
+ // ../core/src/task-priority.ts
5727
+ function isTaskPriority(value) {
5728
+ return typeof value === "string" && TASK_PRIORITIES.includes(value);
5729
+ }
5730
+ function normalizeTaskPriority(priority) {
5731
+ return isTaskPriority(priority) ? priority : DEFAULT_TASK_PRIORITY;
5732
+ }
5733
+ function getTaskPriorityRank(priority) {
5734
+ return PRIORITY_RANK[normalizeTaskPriority(priority)];
5735
+ }
5736
+ function compareTaskPriority(a, b) {
5737
+ return getTaskPriorityRank(b) - getTaskPriorityRank(a);
5738
+ }
5739
+ function compareTaskId(a, b) {
5740
+ const aNum = Number.parseInt(a.slice(a.lastIndexOf("-") + 1), 10);
5741
+ const bNum = Number.parseInt(b.slice(b.lastIndexOf("-") + 1), 10);
5742
+ if (Number.isFinite(aNum) && Number.isFinite(bNum) && aNum !== bNum) {
5743
+ return aNum - bNum;
5744
+ }
5745
+ return a.localeCompare(b);
5746
+ }
5747
+ function compareTasksByPriorityThenAgeAndId(a, b) {
5748
+ const priorityCmp = compareTaskPriority(a.priority, b.priority);
5749
+ if (priorityCmp !== 0) {
5750
+ return priorityCmp;
5751
+ }
5752
+ if (a.createdAt !== b.createdAt) {
5753
+ return a.createdAt.localeCompare(b.createdAt);
5754
+ }
5755
+ return compareTaskId(a.id, b.id);
5756
+ }
5757
+ function sortTasksByPriorityThenAgeAndId(tasks) {
5758
+ return [...tasks].sort(compareTasksByPriorityThenAgeAndId);
5759
+ }
5760
+ var PRIORITY_RANK;
5761
+ var init_task_priority = __esm({
5762
+ "../core/src/task-priority.ts"() {
5763
+ "use strict";
5764
+ init_types();
5765
+ PRIORITY_RANK = {
5766
+ low: 0,
5767
+ normal: 1,
5768
+ high: 2,
5769
+ urgent: 3
5770
+ };
5771
+ }
5772
+ });
5773
+
5683
5774
  // ../core/src/global-settings.ts
5684
5775
  import { homedir } from "node:os";
5685
5776
  import { dirname, join as join4 } from "node:path";
@@ -6177,17 +6268,18 @@ async function migrateTasks(fusionDir, db) {
6177
6268
  let skipped = 0;
6178
6269
  const insertStmt = db.prepare(`
6179
6270
  INSERT OR REPLACE INTO tasks (
6180
- id, title, description, "column", status, size, reviewLevel, currentStep,
6271
+ id, title, description, priority, "column", status, size, reviewLevel, currentStep,
6181
6272
  worktree, blockedBy, paused, baseBranch, baseCommitSha, modelPresetId,
6182
6273
  modelProvider, modelId, validatorModelProvider, validatorModelId,
6183
6274
  mergeRetries, recoveryRetryCount, nextRecoveryAt,
6184
6275
  error, summary, thinkingLevel, createdAt, updatedAt,
6185
6276
  columnMovedAt, dependencies, steps, log, attachments, steeringComments,
6186
- comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
6187
- breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
6277
+ comments, workflowStepResults, prInfo, issueInfo,
6278
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
6279
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, sliceId
6188
6280
  ) VALUES (
6189
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
6190
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
6281
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
6282
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
6191
6283
  )
6192
6284
  `);
6193
6285
  for (const entry of entries) {
@@ -6205,6 +6297,7 @@ async function migrateTasks(fusionDir, db) {
6205
6297
  task.id,
6206
6298
  task.title ?? null,
6207
6299
  task.description,
6300
+ normalizeTaskPriority(task.priority),
6208
6301
  task.column,
6209
6302
  task.status ?? null,
6210
6303
  task.size ?? null,
@@ -6238,6 +6331,11 @@ async function migrateTasks(fusionDir, db) {
6238
6331
  toJson(task.workflowStepResults || []),
6239
6332
  toJsonNullable(task.prInfo),
6240
6333
  toJsonNullable(task.issueInfo),
6334
+ task.sourceIssue?.provider ?? null,
6335
+ task.sourceIssue?.repository ?? null,
6336
+ task.sourceIssue?.externalIssueId ?? null,
6337
+ task.sourceIssue?.issueNumber ?? null,
6338
+ task.sourceIssue?.url ?? null,
6241
6339
  toJsonNullable(task.mergeDetails),
6242
6340
  task.breakIntoSubtasks ? 1 : 0,
6243
6341
  toJson(task.enabledWorkflowSteps || []),
@@ -6488,6 +6586,7 @@ var init_db_migrate = __esm({
6488
6586
  "../core/src/db-migrate.ts"() {
6489
6587
  "use strict";
6490
6588
  init_db();
6589
+ init_task_priority();
6491
6590
  }
6492
6591
  });
6493
6592
 
@@ -20322,7 +20421,7 @@ var require_luxon = __commonJS({
20322
20421
  }, zone || mergedZone, next];
20323
20422
  }, [{}, null, 1]).slice(0, 2);
20324
20423
  }
20325
- function parse2(s2, ...patterns) {
20424
+ function parse(s2, ...patterns) {
20326
20425
  if (s2 == null) {
20327
20426
  return [null, null];
20328
20427
  }
@@ -20465,26 +20564,26 @@ var require_luxon = __commonJS({
20465
20564
  var extractISOOrdinalDateAndTime = combineExtractors(extractISOOrdinalData, extractISOTime, extractISOOffset, extractIANAZone);
20466
20565
  var extractISOTimeAndOffset = combineExtractors(extractISOTime, extractISOOffset, extractIANAZone);
20467
20566
  function parseISODate(s2) {
20468
- return parse2(s2, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], [isoTimeCombinedRegex, extractISOTimeAndOffset]);
20567
+ return parse(s2, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], [isoTimeCombinedRegex, extractISOTimeAndOffset]);
20469
20568
  }
20470
20569
  function parseRFC2822Date(s2) {
20471
- return parse2(preprocessRFC2822(s2), [rfc2822, extractRFC2822]);
20570
+ return parse(preprocessRFC2822(s2), [rfc2822, extractRFC2822]);
20472
20571
  }
20473
20572
  function parseHTTPDate(s2) {
20474
- return parse2(s2, [rfc1123, extractRFC1123Or850], [rfc850, extractRFC1123Or850], [ascii, extractASCII]);
20573
+ return parse(s2, [rfc1123, extractRFC1123Or850], [rfc850, extractRFC1123Or850], [ascii, extractASCII]);
20475
20574
  }
20476
20575
  function parseISODuration(s2) {
20477
- return parse2(s2, [isoDuration, extractISODuration]);
20576
+ return parse(s2, [isoDuration, extractISODuration]);
20478
20577
  }
20479
20578
  var extractISOTimeOnly = combineExtractors(extractISOTime);
20480
20579
  function parseISOTimeOnly(s2) {
20481
- return parse2(s2, [isoTimeOnly, extractISOTimeOnly]);
20580
+ return parse(s2, [isoTimeOnly, extractISOTimeOnly]);
20482
20581
  }
20483
20582
  var sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex);
20484
20583
  var sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex);
20485
20584
  var extractISOTimeOffsetAndIANAZone = combineExtractors(extractISOTime, extractISOOffset, extractIANAZone);
20486
20585
  function parseSQL(s2) {
20487
- return parse2(s2, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]);
20586
+ return parse(s2, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone]);
20488
20587
  }
20489
20588
  var INVALID$2 = "Invalid Duration";
20490
20589
  var lowOrderMatrix = {
@@ -26807,13 +26906,14 @@ __export(automation_store_exports, {
26807
26906
  import { EventEmitter as EventEmitter10 } from "node:events";
26808
26907
  import { join as join12 } from "node:path";
26809
26908
  import { randomUUID as randomUUID5 } from "node:crypto";
26810
- var import_cron_parser, AutomationStore;
26909
+ var import_cron_parser, CRON_TIMEZONE, AutomationStore;
26811
26910
  var init_automation_store = __esm({
26812
26911
  "../core/src/automation-store.ts"() {
26813
26912
  "use strict";
26814
26913
  import_cron_parser = __toESM(require_dist2(), 1);
26815
26914
  init_automation();
26816
26915
  init_db();
26916
+ CRON_TIMEZONE = "UTC";
26817
26917
  AutomationStore = class _AutomationStore extends EventEmitter10 {
26818
26918
  constructor(rootDir) {
26819
26919
  super();
@@ -26931,7 +27031,8 @@ var init_automation_store = __esm({
26931
27031
  */
26932
27032
  computeNextRun(cronExpression, fromDate) {
26933
27033
  const interval = import_cron_parser.CronExpressionParser.parse(cronExpression, {
26934
- currentDate: fromDate ?? /* @__PURE__ */ new Date()
27034
+ currentDate: fromDate ?? /* @__PURE__ */ new Date(),
27035
+ tz: CRON_TIMEZONE
26935
27036
  });
26936
27037
  const next = interval.next();
26937
27038
  return next.toISOString() ?? new Date(next.getTime()).toISOString();
@@ -28551,8 +28652,8 @@ This project has OpenClaw-style memory files:
28551
28652
  - \`.fusion/memory/YYYY-MM-DD.md\` \u2014 append-only daily notes for running context
28552
28653
 
28553
28654
  **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
28655
+ 1. Use \`fn_memory_search\` first for task-relevant context
28656
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28556
28657
  3. Incorporate relevant learnings into your specification \u2014 reference actual patterns, constraints, and conventions documented there
28557
28658
 
28558
28659
  Do not read all memory directly by default. If memory is irrelevant, skip it.
@@ -28564,8 +28665,8 @@ Do not read all memory directly by default. If memory is irrelevant, skip it.
28564
28665
  This project has a memory system that stores durable project learnings.
28565
28666
 
28566
28667
  **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
28668
+ 1. Use \`fn_memory_search\` first for task-relevant context
28669
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28569
28670
  3. Incorporate useful learnings into your specification
28570
28671
 
28571
28672
  **If the memory contains useful context for this task, reference it in the specification.**
@@ -28597,12 +28698,12 @@ This project has OpenClaw-style memory files:
28597
28698
  - \`.fusion/memory/YYYY-MM-DD.md\` \u2014 append-only daily notes for running observations and open loops
28598
28699
 
28599
28700
  **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
28701
+ 1. Use \`fn_memory_search\` first for task-relevant context
28702
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28602
28703
  3. Apply relevant learnings to your implementation \u2014 follow documented patterns and avoid known pitfalls
28603
28704
  4. Do not load all memory directly by default. Skip memory reads when memory is irrelevant or context is tight.
28604
28705
 
28605
- **At the end of execution (before calling \`task_done()\`):**
28706
+ **At the end of execution (before calling \`fn_task_done()\`):**
28606
28707
  1. Review what you learned during this task that would genuinely benefit future runs
28607
28708
  2. Write durable decisions, conventions, and pitfalls to \`.fusion/memory/MEMORY.md\`
28608
28709
  3. Write running observations, unresolved context, and open loops to today's \`.fusion/memory/YYYY-MM-DD.md\`
@@ -28631,11 +28732,11 @@ This project has OpenClaw-style memory files:
28631
28732
  This project has a memory system that stores durable project learnings accumulated from past task runs.
28632
28733
 
28633
28734
  **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
28735
+ 1. Use \`fn_memory_search\` first for task-relevant context
28736
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28636
28737
  3. Apply useful learnings to your implementation
28637
28738
 
28638
- **At the end of execution (before calling \`task_done()\`):**
28739
+ **At the end of execution (before calling \`fn_task_done()\`):**
28639
28740
  1. Review what you learned during this task that would genuinely benefit future runs
28640
28741
  2. **If nothing durable was learned, skip the memory update entirely** \u2014 do not append trivial or task-specific notes
28641
28742
  3. Only write when you have genuinely durable, reusable insights such as:
@@ -28663,8 +28764,8 @@ function buildReviewerMemoryInstructions(rootDir, settings) {
28663
28764
  This project has a memory system that stores durable project learnings.
28664
28765
 
28665
28766
  **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
28767
+ 1. Use \`fn_memory_search\` for task-relevant project conventions, pitfalls, and prior decisions when they could affect your verdict
28768
+ 2. Use \`fn_memory_get\` only for specific memory files/line ranges returned by search
28668
28769
  3. Treat documented durable conventions and pitfalls as review evidence when deciding APPROVE, REVISE, or RETHINK
28669
28770
  4. Do not update memory during review; reviewer memory access is read-only
28670
28771
  5. Skip memory reads when they are not relevant to the reviewed plan or code
@@ -28786,23 +28887,29 @@ var init_run_command = __esm({
28786
28887
  });
28787
28888
 
28788
28889
  // ../core/src/logger.ts
28890
+ function withSeverityMarker(level, payload) {
28891
+ return `${LOG_LEVEL_MARKER_PREFIX}${level}${LOG_LEVEL_MARKER_SUFFIX}${payload}`;
28892
+ }
28789
28893
  function createLogger(prefix) {
28790
28894
  const tag = `[${prefix}]`;
28791
28895
  return {
28792
28896
  log(message, ...args) {
28793
- console.error(`${tag} ${message}`, ...args);
28897
+ console.error(withSeverityMarker("info", `${tag} ${message}`), ...args);
28794
28898
  },
28795
28899
  warn(message, ...args) {
28796
- console.warn(`${tag} ${message}`, ...args);
28900
+ console.warn(withSeverityMarker("warn", `${tag} ${message}`), ...args);
28797
28901
  },
28798
28902
  error(message, ...args) {
28799
- console.error(`${tag} ${message}`, ...args);
28903
+ console.error(withSeverityMarker("error", `${tag} ${message}`), ...args);
28800
28904
  }
28801
28905
  };
28802
28906
  }
28907
+ var LOG_LEVEL_MARKER_PREFIX, LOG_LEVEL_MARKER_SUFFIX;
28803
28908
  var init_logger = __esm({
28804
28909
  "../core/src/logger.ts"() {
28805
28910
  "use strict";
28911
+ LOG_LEVEL_MARKER_PREFIX = "\0fnlvl=";
28912
+ LOG_LEVEL_MARKER_SUFFIX = "\0";
28806
28913
  }
28807
28914
  });
28808
28915
 
@@ -28854,6 +28961,7 @@ var init_store = __esm({
28854
28961
  "../core/src/store.ts"() {
28855
28962
  "use strict";
28856
28963
  init_types();
28964
+ init_task_priority();
28857
28965
  init_global_settings();
28858
28966
  init_db();
28859
28967
  init_archive_db();
@@ -29043,6 +29151,7 @@ var init_store = __esm({
29043
29151
  id: row.id,
29044
29152
  title: row.title || void 0,
29045
29153
  description: row.description,
29154
+ priority: normalizeTaskPriority(row.priority),
29046
29155
  column: row.column,
29047
29156
  status: row.status || void 0,
29048
29157
  size: row.size || void 0,
@@ -29078,6 +29187,19 @@ var init_store = __esm({
29078
29187
  dependencies: fromJson(row.dependencies) || [],
29079
29188
  steps: fromJson(row.steps) || [],
29080
29189
  log: fromJson(row.log) || [],
29190
+ tokenUsage: (() => {
29191
+ if (row.tokenUsageInputTokens === null || row.tokenUsageOutputTokens === null || row.tokenUsageCachedTokens === null || row.tokenUsageTotalTokens === null || row.tokenUsageFirstUsedAt === null || row.tokenUsageLastUsedAt === null) {
29192
+ return void 0;
29193
+ }
29194
+ return {
29195
+ inputTokens: row.tokenUsageInputTokens,
29196
+ outputTokens: row.tokenUsageOutputTokens,
29197
+ cachedTokens: row.tokenUsageCachedTokens,
29198
+ totalTokens: row.tokenUsageTotalTokens,
29199
+ firstUsedAt: row.tokenUsageFirstUsedAt,
29200
+ lastUsedAt: row.tokenUsageLastUsedAt
29201
+ };
29202
+ })(),
29081
29203
  attachments: (() => {
29082
29204
  const a = fromJson(row.attachments);
29083
29205
  return a && a.length > 0 ? a : void 0;
@@ -29102,6 +29224,18 @@ var init_store = __esm({
29102
29224
  })(),
29103
29225
  prInfo: fromJson(row.prInfo),
29104
29226
  issueInfo: fromJson(row.issueInfo),
29227
+ sourceIssue: (() => {
29228
+ if (row.sourceIssueProvider === null || row.sourceIssueRepository === null || row.sourceIssueExternalIssueId === null || row.sourceIssueNumber === null) {
29229
+ return void 0;
29230
+ }
29231
+ return {
29232
+ provider: row.sourceIssueProvider,
29233
+ repository: row.sourceIssueRepository,
29234
+ externalIssueId: row.sourceIssueExternalIssueId,
29235
+ issueNumber: row.sourceIssueNumber,
29236
+ url: row.sourceIssueUrl ?? void 0
29237
+ };
29238
+ })(),
29105
29239
  mergeDetails: fromJson(row.mergeDetails),
29106
29240
  breakIntoSubtasks: row.breakIntoSubtasks ? true : void 0,
29107
29241
  enabledWorkflowSteps: (() => {
@@ -29125,6 +29259,7 @@ var init_store = __esm({
29125
29259
  id: entry.id,
29126
29260
  title: entry.title,
29127
29261
  description: entry.description,
29262
+ priority: normalizeTaskPriority(entry.priority),
29128
29263
  column: "archived",
29129
29264
  dependencies: entry.dependencies ?? [],
29130
29265
  steps: entry.steps ?? [],
@@ -29133,6 +29268,7 @@ var init_store = __esm({
29133
29268
  reviewLevel: entry.reviewLevel,
29134
29269
  prInfo: slim ? void 0 : entry.prInfo,
29135
29270
  issueInfo: slim ? void 0 : entry.issueInfo,
29271
+ sourceIssue: slim ? void 0 : entry.sourceIssue,
29136
29272
  attachments: slim ? void 0 : entry.attachments,
29137
29273
  comments: entry.comments,
29138
29274
  log: slim ? [] : entry.log ?? [],
@@ -29221,6 +29357,7 @@ ${recentText}` : void 0
29221
29357
  id: task.id,
29222
29358
  title: task.title,
29223
29359
  description: task.description,
29360
+ priority: normalizeTaskPriority(task.priority),
29224
29361
  column: "archived",
29225
29362
  dependencies: task.dependencies,
29226
29363
  steps: task.steps,
@@ -29229,6 +29366,7 @@ ${recentText}` : void 0
29229
29366
  reviewLevel: task.reviewLevel,
29230
29367
  prInfo: task.prInfo,
29231
29368
  issueInfo: task.issueInfo,
29369
+ sourceIssue: task.sourceIssue,
29232
29370
  attachments: task.attachments,
29233
29371
  comments: task.comments,
29234
29372
  prompt,
@@ -29297,6 +29435,7 @@ ${recentText}` : void 0
29297
29435
  "id",
29298
29436
  "title",
29299
29437
  "description",
29438
+ "priority",
29300
29439
  '"column"',
29301
29440
  "status",
29302
29441
  "size",
@@ -29326,6 +29465,12 @@ ${recentText}` : void 0
29326
29465
  "summary",
29327
29466
  "thinkingLevel",
29328
29467
  "executionMode",
29468
+ "tokenUsageInputTokens",
29469
+ "tokenUsageOutputTokens",
29470
+ "tokenUsageCachedTokens",
29471
+ "tokenUsageTotalTokens",
29472
+ "tokenUsageFirstUsedAt",
29473
+ "tokenUsageLastUsedAt",
29329
29474
  "createdAt",
29330
29475
  "updatedAt",
29331
29476
  "columnMovedAt",
@@ -29337,6 +29482,11 @@ ${recentText}` : void 0
29337
29482
  "attachments",
29338
29483
  "prInfo",
29339
29484
  "issueInfo",
29485
+ "sourceIssueProvider",
29486
+ "sourceIssueRepository",
29487
+ "sourceIssueExternalIssueId",
29488
+ "sourceIssueNumber",
29489
+ "sourceIssueUrl",
29340
29490
  "mergeDetails",
29341
29491
  "breakIntoSubtasks",
29342
29492
  "enabledWorkflowSteps",
@@ -29354,6 +29504,7 @@ ${recentText}` : void 0
29354
29504
  "id",
29355
29505
  "title",
29356
29506
  "description",
29507
+ "priority",
29357
29508
  '"column"',
29358
29509
  "status",
29359
29510
  "size",
@@ -29383,6 +29534,12 @@ ${recentText}` : void 0
29383
29534
  "summary",
29384
29535
  "thinkingLevel",
29385
29536
  "executionMode",
29537
+ "tokenUsageInputTokens",
29538
+ "tokenUsageOutputTokens",
29539
+ "tokenUsageCachedTokens",
29540
+ "tokenUsageTotalTokens",
29541
+ "tokenUsageFirstUsedAt",
29542
+ "tokenUsageLastUsedAt",
29386
29543
  "createdAt",
29387
29544
  "updatedAt",
29388
29545
  "columnMovedAt",
@@ -29394,6 +29551,11 @@ ${recentText}` : void 0
29394
29551
  "workflowStepResults",
29395
29552
  "prInfo",
29396
29553
  "issueInfo",
29554
+ "sourceIssueProvider",
29555
+ "sourceIssueRepository",
29556
+ "sourceIssueExternalIssueId",
29557
+ "sourceIssueNumber",
29558
+ "sourceIssueUrl",
29397
29559
  "mergeDetails",
29398
29560
  "breakIntoSubtasks",
29399
29561
  "enabledWorkflowSteps",
@@ -29431,21 +29593,23 @@ ${recentText}` : void 0
29431
29593
  upsertTask(task) {
29432
29594
  this.db.prepare(`
29433
29595
  INSERT INTO tasks (
29434
- id, title, description, "column", status, size, reviewLevel, currentStep,
29596
+ id, title, description, priority, "column", status, size, reviewLevel, currentStep,
29435
29597
  worktree, blockedBy, paused, baseBranch, branch, baseCommitSha, modelPresetId, modelProvider,
29436
29598
  modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
29437
29599
  workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, nextRecoveryAt, error,
29438
- summary, thinkingLevel, executionMode, createdAt, updatedAt, columnMovedAt,
29600
+ summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
29601
+ tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
29439
29602
  dependencies, steps, log, attachments, steeringComments,
29440
- comments, workflowStepResults, prInfo, issueInfo, mergeDetails,
29441
- breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
29603
+ comments, workflowStepResults, prInfo, issueInfo,
29604
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
29605
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, checkedOutBy, checkedOutAt
29442
29606
  ) VALUES (
29443
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
29444
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
29607
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
29445
29608
  )
29446
29609
  ON CONFLICT(id) DO UPDATE SET
29447
29610
  title = excluded.title,
29448
29611
  description = excluded.description,
29612
+ priority = excluded.priority,
29449
29613
  "column" = excluded."column",
29450
29614
  status = excluded.status,
29451
29615
  size = excluded.size,
@@ -29475,6 +29639,12 @@ ${recentText}` : void 0
29475
29639
  summary = excluded.summary,
29476
29640
  thinkingLevel = excluded.thinkingLevel,
29477
29641
  executionMode = excluded.executionMode,
29642
+ tokenUsageInputTokens = excluded.tokenUsageInputTokens,
29643
+ tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
29644
+ tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
29645
+ tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
29646
+ tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
29647
+ tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
29478
29648
  createdAt = excluded.createdAt,
29479
29649
  updatedAt = excluded.updatedAt,
29480
29650
  columnMovedAt = excluded.columnMovedAt,
@@ -29487,6 +29657,11 @@ ${recentText}` : void 0
29487
29657
  workflowStepResults = excluded.workflowStepResults,
29488
29658
  prInfo = excluded.prInfo,
29489
29659
  issueInfo = excluded.issueInfo,
29660
+ sourceIssueProvider = excluded.sourceIssueProvider,
29661
+ sourceIssueRepository = excluded.sourceIssueRepository,
29662
+ sourceIssueExternalIssueId = excluded.sourceIssueExternalIssueId,
29663
+ sourceIssueNumber = excluded.sourceIssueNumber,
29664
+ sourceIssueUrl = excluded.sourceIssueUrl,
29490
29665
  mergeDetails = excluded.mergeDetails,
29491
29666
  breakIntoSubtasks = excluded.breakIntoSubtasks,
29492
29667
  enabledWorkflowSteps = excluded.enabledWorkflowSteps,
@@ -29501,6 +29676,7 @@ ${recentText}` : void 0
29501
29676
  task.id,
29502
29677
  task.title ?? null,
29503
29678
  task.description,
29679
+ normalizeTaskPriority(task.priority),
29504
29680
  task.column,
29505
29681
  task.status ?? null,
29506
29682
  task.size ?? null,
@@ -29530,6 +29706,12 @@ ${recentText}` : void 0
29530
29706
  task.summary ?? null,
29531
29707
  task.thinkingLevel ?? null,
29532
29708
  task.executionMode ?? null,
29709
+ task.tokenUsage?.inputTokens ?? null,
29710
+ task.tokenUsage?.outputTokens ?? null,
29711
+ task.tokenUsage?.cachedTokens ?? null,
29712
+ task.tokenUsage?.totalTokens ?? null,
29713
+ task.tokenUsage?.firstUsedAt ?? null,
29714
+ task.tokenUsage?.lastUsedAt ?? null,
29533
29715
  task.createdAt,
29534
29716
  task.updatedAt,
29535
29717
  task.columnMovedAt ?? null,
@@ -29542,6 +29724,11 @@ ${recentText}` : void 0
29542
29724
  toJson(task.workflowStepResults || []),
29543
29725
  toJsonNullable(task.prInfo),
29544
29726
  toJsonNullable(task.issueInfo),
29727
+ task.sourceIssue?.provider ?? null,
29728
+ task.sourceIssue?.repository ?? null,
29729
+ task.sourceIssue?.externalIssueId ?? null,
29730
+ task.sourceIssue?.issueNumber ?? null,
29731
+ task.sourceIssue?.url ?? null,
29545
29732
  toJsonNullable(task.mergeDetails),
29546
29733
  task.breakIntoSubtasks ? 1 : 0,
29547
29734
  toJson(task.enabledWorkflowSteps || []),
@@ -29746,6 +29933,7 @@ ${recentText}` : void 0
29746
29933
  if (!Array.isArray(fileTask.log)) fileTask.log = [];
29747
29934
  if (!Array.isArray(fileTask.dependencies)) fileTask.dependencies = [];
29748
29935
  if (!Array.isArray(fileTask.steps)) fileTask.steps = [];
29936
+ fileTask.priority = normalizeTaskPriority(fileTask.priority);
29749
29937
  return fileTask;
29750
29938
  } catch (err) {
29751
29939
  throw new Error(
@@ -30280,6 +30468,9 @@ ${recentText}` : void 0
30280
30468
  id,
30281
30469
  title,
30282
30470
  description: input.description,
30471
+ priority: normalizeTaskPriority(input.priority),
30472
+ tokenUsage: input.tokenUsage,
30473
+ sourceIssue: input.sourceIssue,
30283
30474
  column: input.column || "triage",
30284
30475
  dependencies: input.dependencies || [],
30285
30476
  breakIntoSubtasks: input.breakIntoSubtasks === true ? true : void 0,
@@ -30334,6 +30525,7 @@ ${task.description}
30334
30525
  description: `${sourceTask.description}
30335
30526
 
30336
30527
  (Duplicated from ${id})`,
30528
+ priority: normalizeTaskPriority(sourceTask.priority),
30337
30529
  column: "triage",
30338
30530
  modelPresetId: sourceTask.modelPresetId,
30339
30531
  dependencies: [],
@@ -30392,6 +30584,7 @@ ${task.description}
30392
30584
  description: `${feedback.trim()}
30393
30585
 
30394
30586
  Refines: ${id}`,
30587
+ priority: normalizeTaskPriority(sourceTask.priority),
30395
30588
  column: "triage",
30396
30589
  dependencies: [id],
30397
30590
  // Refinement depends on the original being complete
@@ -30716,6 +30909,11 @@ ${newTask.description}
30716
30909
  }
30717
30910
  if (updates.title !== void 0) task.title = updates.title;
30718
30911
  if (updates.description !== void 0) task.description = updates.description;
30912
+ if (updates.priority === null) {
30913
+ task.priority = normalizeTaskPriority(void 0);
30914
+ } else if (updates.priority !== void 0) {
30915
+ task.priority = normalizeTaskPriority(updates.priority);
30916
+ }
30719
30917
  if (updates.worktree === null) {
30720
30918
  task.worktree = void 0;
30721
30919
  } else if (updates.worktree !== void 0) {
@@ -30883,6 +31081,16 @@ ${newTask.description}
30883
31081
  } else if (updates.mergeDetails !== void 0) {
30884
31082
  task.mergeDetails = updates.mergeDetails;
30885
31083
  }
31084
+ if (updates.sourceIssue === null) {
31085
+ task.sourceIssue = void 0;
31086
+ } else if (updates.sourceIssue !== void 0) {
31087
+ task.sourceIssue = updates.sourceIssue;
31088
+ }
31089
+ if (updates.tokenUsage === null) {
31090
+ task.tokenUsage = void 0;
31091
+ } else if (updates.tokenUsage !== void 0) {
31092
+ task.tokenUsage = updates.tokenUsage;
31093
+ }
30886
31094
  if (updates.modifiedFiles === null) {
30887
31095
  task.modifiedFiles = void 0;
30888
31096
  } else if (updates.modifiedFiles !== void 0) {
@@ -31283,14 +31491,14 @@ ${task.description}
31283
31491
  }
31284
31492
  return paths;
31285
31493
  }
31286
- async deleteTask(id) {
31494
+ async deleteTask(id, options) {
31287
31495
  return this.withTaskLock(id, async () => {
31288
31496
  const task = this.readTaskFromDb(id);
31289
31497
  if (!task) {
31290
31498
  throw new Error(`Task ${id} not found`);
31291
31499
  }
31292
31500
  const dependentIds = this.findLiveDependents(id);
31293
- if (dependentIds.length > 0) {
31501
+ if (dependentIds.length > 0 && !options?.removeDependencyReferences) {
31294
31502
  throw new TaskHasDependentsError(id, dependentIds);
31295
31503
  }
31296
31504
  const cleanedBranches = await this.cleanupBranchForTask(task);
@@ -31301,18 +31509,50 @@ ${task.description}
31301
31509
  action: `Cleaned up branch: ${cleanedBranches.join(", ")}`
31302
31510
  });
31303
31511
  }
31304
- this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
31305
- this.db.bumpLastModified();
31512
+ const rewrittenDependents = this.rewriteDependentsAndDeleteTask(id, dependentIds);
31306
31513
  if (this.isWatching) this.taskCache.delete(id);
31307
31514
  const dir = this.taskDir(id);
31308
31515
  if (existsSync12(dir)) {
31309
- const { rm: rm2 } = await import("node:fs/promises");
31310
- await rm2(dir, { recursive: true });
31516
+ const { rm: rm3 } = await import("node:fs/promises");
31517
+ await rm3(dir, { recursive: true });
31518
+ }
31519
+ for (const dependentTask of rewrittenDependents) {
31520
+ this.emit("task:updated", dependentTask);
31311
31521
  }
31312
31522
  this.emit("task:deleted", task);
31313
31523
  return task;
31314
31524
  });
31315
31525
  }
31526
+ rewriteDependentsAndDeleteTask(taskId, dependentIds) {
31527
+ const rewrittenDependents = [];
31528
+ this.db.transaction(() => {
31529
+ for (const dependentId of dependentIds) {
31530
+ const dependentTask = this.readTaskFromDb(dependentId);
31531
+ if (!dependentTask) continue;
31532
+ const nextDependencies = dependentTask.dependencies.filter((dependencyId) => dependencyId !== taskId);
31533
+ if (nextDependencies.length === dependentTask.dependencies.length) {
31534
+ continue;
31535
+ }
31536
+ const updatedDependent = {
31537
+ ...dependentTask,
31538
+ dependencies: nextDependencies,
31539
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
31540
+ };
31541
+ this.db.prepare("UPDATE tasks SET dependencies = ?, updatedAt = ? WHERE id = ?").run(
31542
+ toJson(updatedDependent.dependencies),
31543
+ updatedDependent.updatedAt,
31544
+ updatedDependent.id
31545
+ );
31546
+ if (this.isWatching) {
31547
+ this.taskCache.set(updatedDependent.id, updatedDependent);
31548
+ }
31549
+ rewrittenDependents.push(updatedDependent);
31550
+ }
31551
+ this.db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
31552
+ this.db.bumpLastModified();
31553
+ });
31554
+ return rewrittenDependents;
31555
+ }
31316
31556
  /**
31317
31557
  * Clean up the git branch associated with a task.
31318
31558
  *
@@ -31609,8 +31849,8 @@ ${task.description}
31609
31849
  this.archiveDb.upsert(entry);
31610
31850
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
31611
31851
  this.db.bumpLastModified();
31612
- const { rm: rm2 } = await import("node:fs/promises");
31613
- await rm2(dir, { recursive: true, force: true });
31852
+ const { rm: rm3 } = await import("node:fs/promises");
31853
+ await rm3(dir, { recursive: true, force: true });
31614
31854
  if (this.isWatching) {
31615
31855
  this.taskCache.delete(id);
31616
31856
  }
@@ -32088,24 +32328,64 @@ ${task.description}
32088
32328
  this.emit("task:updated", task2);
32089
32329
  return task2;
32090
32330
  });
32331
+ const commentContextBase = {
32332
+ taskId: id,
32333
+ author,
32334
+ commentLength: text.length,
32335
+ column: task.column,
32336
+ priorStatus: task.status ?? null
32337
+ };
32338
+ if (runContext) {
32339
+ commentContextBase.runId = runContext.runId;
32340
+ commentContextBase.agentId = runContext.agentId;
32341
+ if (runContext.source) {
32342
+ commentContextBase.runSource = runContext.source;
32343
+ }
32344
+ }
32091
32345
  if (task.column === "done" && author === "user" && !options?.skipRefinement) {
32092
32346
  try {
32093
32347
  await this.refineTask(id, text);
32094
- } catch {
32348
+ } catch (err) {
32349
+ storeLog.warn("Best-effort post-comment auto-refinement failed", {
32350
+ ...commentContextBase,
32351
+ phase: "addComment:auto-refinement",
32352
+ error: err instanceof Error ? err.message : String(err)
32353
+ });
32095
32354
  }
32096
32355
  }
32097
32356
  if (task.column === "triage" && task.status === "awaiting-approval" && author === "user") {
32357
+ let invalidatedStatus = false;
32098
32358
  try {
32099
32359
  await this.updateTask(id, {
32100
32360
  status: "needs-respecify"
32101
32361
  });
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 {
32362
+ invalidatedStatus = true;
32363
+ } catch (err) {
32364
+ storeLog.warn("Best-effort post-comment awaiting-approval invalidation failed", {
32365
+ ...commentContextBase,
32366
+ phase: "addComment:awaiting-approval-invalidation",
32367
+ stage: "status-update",
32368
+ nextStatus: "needs-respecify",
32369
+ error: err instanceof Error ? err.message : String(err)
32370
+ });
32371
+ }
32372
+ if (invalidatedStatus) {
32373
+ try {
32374
+ await this.logEntry(
32375
+ id,
32376
+ `User comment invalidated spec approval \u2014 task needs re-specification`,
32377
+ void 0,
32378
+ runContext
32379
+ );
32380
+ } catch (err) {
32381
+ storeLog.warn("Best-effort post-comment awaiting-approval invalidation failed", {
32382
+ ...commentContextBase,
32383
+ phase: "addComment:awaiting-approval-invalidation",
32384
+ stage: "post-invalidation-log-entry",
32385
+ nextStatus: "needs-respecify",
32386
+ error: err instanceof Error ? err.message : String(err)
32387
+ });
32388
+ }
32109
32389
  }
32110
32390
  }
32111
32391
  return task;
@@ -32381,7 +32661,7 @@ ${task.description}
32381
32661
  const rows2 = this.db.prepare(`
32382
32662
  SELECT * FROM agentLogEntries
32383
32663
  WHERE taskId = ?
32384
- ORDER BY timestamp DESC
32664
+ ORDER BY timestamp DESC, id DESC
32385
32665
  LIMIT ?
32386
32666
  `).all(taskId, readCount);
32387
32667
  const entries2 = rows2.map((row) => this.mapAgentLogRow(row)).reverse();
@@ -32393,7 +32673,7 @@ ${task.description}
32393
32673
  const rows = this.db.prepare(`
32394
32674
  SELECT * FROM agentLogEntries
32395
32675
  WHERE taskId = ?
32396
- ORDER BY timestamp ASC
32676
+ ORDER BY timestamp ASC, id ASC
32397
32677
  `).all(taskId);
32398
32678
  const entries = rows.map((row) => this.mapAgentLogRow(row));
32399
32679
  if (offset > 0) {
@@ -32426,7 +32706,7 @@ ${task.description}
32426
32706
  const rows = this.db.prepare(`
32427
32707
  SELECT * FROM agentLogEntries
32428
32708
  WHERE taskId = ? AND timestamp >= ? AND timestamp <= ?
32429
- ORDER BY timestamp ASC
32709
+ ORDER BY timestamp ASC, id ASC
32430
32710
  `).all(taskId, startIso, end);
32431
32711
  return rows.map((row) => this.mapAgentLogRow(row));
32432
32712
  }
@@ -32522,14 +32802,14 @@ ${task.description}
32522
32802
  if (rows.length === 0) {
32523
32803
  return;
32524
32804
  }
32525
- const { rm: rm2 } = await import("node:fs/promises");
32805
+ const { rm: rm3 } = await import("node:fs/promises");
32526
32806
  for (const row of rows) {
32527
32807
  const task = this.rowToTask(row);
32528
32808
  const archivedAt = task.columnMovedAt ?? task.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
32529
32809
  const entry = await this.taskToArchiveEntry(task, archivedAt);
32530
32810
  this.archiveDb.upsert(entry);
32531
32811
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
32532
- await rm2(this.taskDir(task.id), { recursive: true, force: true });
32812
+ await rm3(this.taskDir(task.id), { recursive: true, force: true });
32533
32813
  if (this.isWatching) {
32534
32814
  this.taskCache.delete(task.id);
32535
32815
  }
@@ -32552,8 +32832,8 @@ ${task.description}
32552
32832
  this.archiveDb.upsert(entry);
32553
32833
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
32554
32834
  this.db.bumpLastModified();
32555
- const { rm: rm2 } = await import("node:fs/promises");
32556
- await rm2(dir, { recursive: true, force: true });
32835
+ const { rm: rm3 } = await import("node:fs/promises");
32836
+ await rm3(dir, { recursive: true, force: true });
32557
32837
  if (this.isWatching) {
32558
32838
  this.taskCache.delete(task.id);
32559
32839
  }
@@ -32576,6 +32856,7 @@ ${task.description}
32576
32856
  id: entry.id,
32577
32857
  title: entry.title,
32578
32858
  description: entry.description,
32859
+ priority: normalizeTaskPriority(entry.priority),
32579
32860
  column: "archived",
32580
32861
  // Will be changed to "done" by unarchiveTask
32581
32862
  dependencies: entry.dependencies,
@@ -32585,6 +32866,7 @@ ${task.description}
32585
32866
  reviewLevel: entry.reviewLevel,
32586
32867
  prInfo: entry.prInfo,
32587
32868
  issueInfo: entry.issueInfo,
32869
+ sourceIssue: entry.sourceIssue,
32588
32870
  attachments: entry.attachments,
32589
32871
  log: [...entry.log, { timestamp: (/* @__PURE__ */ new Date()).toISOString(), action: "Task restored from archive" }],
32590
32872
  comments: entry.comments,
@@ -33194,6 +33476,28 @@ var init_daemon_token = __esm({
33194
33476
  const settings = await this.settingsStore.getSettings();
33195
33477
  return settings.daemonToken;
33196
33478
  }
33479
+ /**
33480
+ * Retrieve the existing daemon token or create/persist one if missing.
33481
+ *
33482
+ * Safe for concurrent callers: if another process writes the token between
33483
+ * the initial read and generateToken(), this method re-reads and returns the
33484
+ * persisted token instead of failing.
33485
+ */
33486
+ async getOrCreateToken() {
33487
+ const existing = await this.getToken();
33488
+ if (existing) {
33489
+ return existing;
33490
+ }
33491
+ try {
33492
+ return await this.generateToken();
33493
+ } catch (error) {
33494
+ const afterRace = await this.getToken();
33495
+ if (afterRace) {
33496
+ return afterRace;
33497
+ }
33498
+ throw error;
33499
+ }
33500
+ }
33197
33501
  /**
33198
33502
  * Validate that a provided token matches the stored token.
33199
33503
  *
@@ -33637,13 +33941,14 @@ __export(routine_store_exports, {
33637
33941
  });
33638
33942
  import { EventEmitter as EventEmitter12 } from "node:events";
33639
33943
  import { randomUUID as randomUUID7 } from "node:crypto";
33640
- var import_cron_parser2, RoutineStore;
33944
+ var import_cron_parser2, CRON_TIMEZONE2, RoutineStore;
33641
33945
  var init_routine_store = __esm({
33642
33946
  "../core/src/routine-store.ts"() {
33643
33947
  "use strict";
33644
33948
  import_cron_parser2 = __toESM(require_dist2(), 1);
33645
33949
  init_db();
33646
33950
  init_routine();
33951
+ CRON_TIMEZONE2 = "UTC";
33647
33952
  RoutineStore = class _RoutineStore extends EventEmitter12 {
33648
33953
  constructor(rootDir) {
33649
33954
  super();
@@ -33806,7 +34111,8 @@ var init_routine_store = __esm({
33806
34111
  */
33807
34112
  computeNextRun(cronExpression, fromDate) {
33808
34113
  const interval = import_cron_parser2.CronExpressionParser.parse(cronExpression, {
33809
- currentDate: fromDate ?? /* @__PURE__ */ new Date()
34114
+ currentDate: fromDate ?? /* @__PURE__ */ new Date(),
34115
+ tz: CRON_TIMEZONE2
33810
34116
  });
33811
34117
  const next = interval.next();
33812
34118
  return new Date(next.getTime()).toISOString();
@@ -34048,12 +34354,11 @@ var init_routine_store = __esm({
34048
34354
  });
34049
34355
 
34050
34356
  // ../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";
34357
+ import { basename as basename6, dirname as dirname5, extname, isAbsolute as isAbsolute5, resolve as resolve7 } from "node:path";
34358
+ import { copyFile, rm } from "node:fs/promises";
34054
34359
  import { pathToFileURL } from "node:url";
34055
34360
  import { EventEmitter as EventEmitter13 } from "node:events";
34056
- var MINIMUM_FUSION_VERSION, log, PluginLoader;
34361
+ var MINIMUM_FUSION_VERSION, log, moduleImportVersion, PluginLoader;
34057
34362
  var init_plugin_loader = __esm({
34058
34363
  "../core/src/plugin-loader.ts"() {
34059
34364
  "use strict";
@@ -34061,6 +34366,7 @@ var init_plugin_loader = __esm({
34061
34366
  init_logger();
34062
34367
  MINIMUM_FUSION_VERSION = "0.1.0";
34063
34368
  log = createLogger("plugin-loader");
34369
+ moduleImportVersion = 0;
34064
34370
  PluginLoader = class extends EventEmitter13 {
34065
34371
  constructor(options) {
34066
34372
  super();
@@ -34070,8 +34376,6 @@ var init_plugin_loader = __esm({
34070
34376
  plugins = /* @__PURE__ */ new Map();
34071
34377
  /** Cache of dynamically imported modules */
34072
34378
  loadedModules = /* @__PURE__ */ new Map();
34073
- /** Monotonic nonce to guarantee unique cache-busting import URLs. */
34074
- importNonce = 0;
34075
34379
  getProjectRoot() {
34076
34380
  return this.options.taskStore.getRootDir();
34077
34381
  }
@@ -34201,31 +34505,24 @@ var init_plugin_loader = __esm({
34201
34505
  if (!bypassCache && this.loadedModules.has(path)) {
34202
34506
  return this.loadedModules.get(path);
34203
34507
  }
34204
- let importPath = path;
34205
- let tempPath = null;
34508
+ const moduleUrl = pathToFileURL(path).href;
34509
+ let mod;
34206
34510
  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
- });
34511
+ moduleImportVersion += 1;
34512
+ const ext = extname(path);
34513
+ const baseName = basename6(path, ext);
34514
+ const reloadedPath = resolve7(dirname5(path), `.${baseName}.reload-${moduleImportVersion}${ext}`);
34515
+ await copyFile(path, reloadedPath);
34516
+ try {
34517
+ mod = await import(pathToFileURL(reloadedPath).href);
34518
+ } finally {
34519
+ await rm(reloadedPath, { force: true }).catch(() => void 0);
34227
34520
  }
34521
+ } else {
34522
+ mod = await import(moduleUrl);
34228
34523
  }
34524
+ this.loadedModules.set(path, mod);
34525
+ return mod;
34229
34526
  }
34230
34527
  /**
34231
34528
  * Invalidate the module cache for a plugin path.
@@ -34614,7 +34911,7 @@ var init_plugin_loader = __esm({
34614
34911
  });
34615
34912
 
34616
34913
  // ../core/src/backup.ts
34617
- import { cp, mkdir as mkdir8, readdir as readdir6, stat as stat3, unlink as unlink5 } from "node:fs/promises";
34914
+ import { cp, mkdir as mkdir8, readdir as readdir6, stat as stat3, unlink as unlink4 } from "node:fs/promises";
34618
34915
  import { existsSync as existsSync14 } from "node:fs";
34619
34916
  import { join as join17 } from "node:path";
34620
34917
  function generateBackupFilename() {
@@ -34867,7 +35164,7 @@ var init_backup = __esm({
34867
35164
  let deletedCount = 0;
34868
35165
  for (const backup of toDelete) {
34869
35166
  try {
34870
- await unlink5(backup.path);
35167
+ await unlink4(backup.path);
34871
35168
  deletedCount++;
34872
35169
  } catch {
34873
35170
  }
@@ -35577,7 +35874,7 @@ var init_mission_types = __esm({
35577
35874
  // ../core/src/memory-insights.ts
35578
35875
  import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir9 } from "node:fs/promises";
35579
35876
  import { existsSync as existsSync15 } from "node:fs";
35580
- import { dirname as dirname5, join as join18 } from "node:path";
35877
+ import { dirname as dirname6, join as join18 } from "node:path";
35581
35878
  async function readWorkingMemory(rootDir) {
35582
35879
  const filePath = join18(rootDir, MEMORY_WORKING_PATH);
35583
35880
  if (!existsSync15(filePath)) {
@@ -35602,7 +35899,7 @@ async function writeInsightsMemory(rootDir, content) {
35602
35899
  }
35603
35900
  async function writeWorkingMemory(rootDir, content) {
35604
35901
  const filePath = join18(rootDir, MEMORY_WORKING_PATH);
35605
- const dir = dirname5(filePath);
35902
+ const dir = dirname6(filePath);
35606
35903
  if (!existsSync15(dir)) {
35607
35904
  await mkdir9(dir, { recursive: true });
35608
35905
  }
@@ -36447,7 +36744,7 @@ var require_ms = __commonJS({
36447
36744
  options = options || {};
36448
36745
  var type = typeof val;
36449
36746
  if (type === "string" && val.length > 0) {
36450
- return parse2(val);
36747
+ return parse(val);
36451
36748
  } else if (type === "number" && isFinite(val)) {
36452
36749
  return options.long ? fmtLong(val) : fmtShort(val);
36453
36750
  }
@@ -36455,7 +36752,7 @@ var require_ms = __commonJS({
36455
36752
  "val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
36456
36753
  );
36457
36754
  };
36458
- function parse2(str) {
36755
+ function parse(str) {
36459
36756
  str = String(str);
36460
36757
  if (str.length > 100) {
36461
36758
  return;
@@ -46128,7 +46425,7 @@ var require_public_api = __commonJS({
46128
46425
  }
46129
46426
  return doc;
46130
46427
  }
46131
- function parse2(src, reviver, options) {
46428
+ function parse(src, reviver, options) {
46132
46429
  let _reviver = void 0;
46133
46430
  if (typeof reviver === "function") {
46134
46431
  _reviver = reviver;
@@ -46169,7 +46466,7 @@ var require_public_api = __commonJS({
46169
46466
  return value.toString(options);
46170
46467
  return new Document.Document(value, _replacer, options).toString(options);
46171
46468
  }
46172
- exports.parse = parse2;
46469
+ exports.parse = parse;
46173
46470
  exports.parseAllDocuments = parseAllDocuments;
46174
46471
  exports.parseDocument = parseDocument;
46175
46472
  exports.stringify = stringify;
@@ -46270,10 +46567,10 @@ function pushAlias(aliases, value) {
46270
46567
  if (pathRef !== normalized && pathRef.length > 0) {
46271
46568
  aliases.add(pathRef);
46272
46569
  }
46273
- const basename8 = extractPathBasename(value);
46274
- if (basename8) {
46275
- aliases.add(basename8);
46276
- const basenameSlug = slugifyAgentReference(basename8);
46570
+ const basename9 = extractPathBasename(value);
46571
+ if (basename9) {
46572
+ aliases.add(basename9);
46573
+ const basenameSlug = slugifyAgentReference(basename9);
46277
46574
  if (basenameSlug.length > 0) {
46278
46575
  aliases.add(basenameSlug);
46279
46576
  }
@@ -46959,7 +47256,7 @@ var init_agent_companies_exporter = __esm({
46959
47256
 
46960
47257
  // ../core/src/chat-store.ts
46961
47258
  import { EventEmitter as EventEmitter14 } from "node:events";
46962
- import { randomUUID as randomUUID9 } from "node:crypto";
47259
+ import { randomUUID as randomUUID8 } from "node:crypto";
46963
47260
  var ChatStore;
46964
47261
  var init_chat_store = __esm({
46965
47262
  "../core/src/chat-store.ts"() {
@@ -47012,7 +47309,7 @@ var init_chat_store = __esm({
47012
47309
  */
47013
47310
  createSession(input) {
47014
47311
  const now = (/* @__PURE__ */ new Date()).toISOString();
47015
- const id = `chat-${randomUUID9().slice(0, 8)}`;
47312
+ const id = `chat-${randomUUID8().slice(0, 8)}`;
47016
47313
  const session = {
47017
47314
  id,
47018
47315
  agentId: input.agentId,
@@ -47080,6 +47377,58 @@ var init_chat_store = __esm({
47080
47377
  `).all(...params);
47081
47378
  return rows.map((row) => this.rowToSession(row));
47082
47379
  }
47380
+ /**
47381
+ * Find the newest active session for a specific quick-chat target.
47382
+ *
47383
+ * Matching semantics:
47384
+ * - model target (`modelProvider` + `modelId`): exact agent+model match
47385
+ * - agent target (no model): prefer model-less sessions, then newest agent session fallback
47386
+ */
47387
+ findLatestActiveSessionForTarget(options) {
47388
+ const normalizedAgentId = options.agentId.trim();
47389
+ if (!normalizedAgentId) {
47390
+ return void 0;
47391
+ }
47392
+ const normalizedProvider = options.modelProvider?.trim();
47393
+ const normalizedModelId = options.modelId?.trim();
47394
+ if (normalizedProvider && !normalizedModelId || !normalizedProvider && normalizedModelId) {
47395
+ throw new Error("modelProvider and modelId must both be provided together, or neither");
47396
+ }
47397
+ const whereClauses = ["status = ?", "agentId = ?"];
47398
+ const baseParams = ["active", normalizedAgentId];
47399
+ if (options.projectId && options.projectId.trim()) {
47400
+ whereClauses.push("projectId = ?");
47401
+ baseParams.push(options.projectId.trim());
47402
+ }
47403
+ const baseWhereSql = whereClauses.join(" AND ");
47404
+ if (normalizedProvider && normalizedModelId) {
47405
+ const row = this.db.prepare(`
47406
+ SELECT * FROM chat_sessions
47407
+ WHERE ${baseWhereSql} AND modelProvider = ? AND modelId = ?
47408
+ ORDER BY updatedAt DESC
47409
+ LIMIT 1
47410
+ `).get(...baseParams, normalizedProvider, normalizedModelId);
47411
+ return row ? this.rowToSession(row) : void 0;
47412
+ }
47413
+ const modelLessRow = this.db.prepare(`
47414
+ SELECT * FROM chat_sessions
47415
+ WHERE ${baseWhereSql}
47416
+ AND COALESCE(TRIM(modelProvider), '') = ''
47417
+ AND COALESCE(TRIM(modelId), '') = ''
47418
+ ORDER BY updatedAt DESC
47419
+ LIMIT 1
47420
+ `).get(...baseParams);
47421
+ if (modelLessRow) {
47422
+ return this.rowToSession(modelLessRow);
47423
+ }
47424
+ const fallbackRow = this.db.prepare(`
47425
+ SELECT * FROM chat_sessions
47426
+ WHERE ${baseWhereSql}
47427
+ ORDER BY updatedAt DESC
47428
+ LIMIT 1
47429
+ `).get(...baseParams);
47430
+ return fallbackRow ? this.rowToSession(fallbackRow) : void 0;
47431
+ }
47083
47432
  /**
47084
47433
  * Update a chat session.
47085
47434
  *
@@ -47158,7 +47507,7 @@ var init_chat_store = __esm({
47158
47507
  throw new Error(`Chat session ${sessionId} not found`);
47159
47508
  }
47160
47509
  const now = (/* @__PURE__ */ new Date()).toISOString();
47161
- const id = `msg-${randomUUID9().slice(0, 8)}`;
47510
+ const id = `msg-${randomUUID8().slice(0, 8)}`;
47162
47511
  const message = {
47163
47512
  id,
47164
47513
  sessionId,
@@ -47312,6 +47661,7 @@ __export(src_exports, {
47312
47661
  DEFAULT_MIN_INTERVAL_MS: () => DEFAULT_MIN_INTERVAL_MS,
47313
47662
  DEFAULT_PROJECT_SETTINGS: () => DEFAULT_PROJECT_SETTINGS,
47314
47663
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
47664
+ DEFAULT_TASK_PRIORITY: () => DEFAULT_TASK_PRIORITY,
47315
47665
  DaemonTokenManager: () => DaemonTokenManager,
47316
47666
  Database: () => Database,
47317
47667
  EXECUTION_MODES: () => EXECUTION_MODES,
@@ -47367,6 +47717,7 @@ __export(src_exports, {
47367
47717
  SLICE_PLAN_STATES: () => SLICE_PLAN_STATES,
47368
47718
  SLICE_STATUSES: () => SLICE_STATUSES,
47369
47719
  SUMMARIZE_SYSTEM_PROMPT: () => SUMMARIZE_SYSTEM_PROMPT,
47720
+ TASK_PRIORITIES: () => TASK_PRIORITIES,
47370
47721
  THEME_MODES: () => THEME_MODES,
47371
47722
  THINKING_LEVELS: () => THINKING_LEVELS,
47372
47723
  TaskStore: () => TaskStore,
@@ -47399,6 +47750,8 @@ __export(src_exports, {
47399
47750
  clearOverrides: () => clearOverrides,
47400
47751
  collectSystemMetrics: () => collectSystemMetrics,
47401
47752
  compactMemoryWithAi: () => compactMemoryWithAi,
47753
+ compareTaskPriority: () => compareTaskPriority,
47754
+ compareTasksByPriorityThenAgeAndId: () => compareTasksByPriorityThenAgeAndId,
47402
47755
  computeAccessState: () => computeAccessState,
47403
47756
  computeInsightFingerprint: () => computeInsightFingerprint,
47404
47757
  convertAgentCompanies: () => convertAgentCompanies,
@@ -47454,6 +47807,7 @@ __export(src_exports, {
47454
47807
  getRateLimitResetTime: () => getRateLimitResetTime,
47455
47808
  getTaskCompletionBlocker: () => getTaskCompletionBlocker,
47456
47809
  getTaskMergeBlocker: () => getTaskMergeBlocker,
47810
+ getTaskPriorityRank: () => getTaskPriorityRank,
47457
47811
  getTemplatesForRole: () => getTemplatesForRole,
47458
47812
  getValidTransitions: () => getValidTransitions,
47459
47813
  hasAgentIdentity: () => hasAgentIdentity,
@@ -47470,6 +47824,7 @@ __export(src_exports, {
47470
47824
  isManualTrigger: () => isManualTrigger,
47471
47825
  isProjectSettingsKey: () => isProjectSettingsKey,
47472
47826
  isQmdAvailable: () => isQmdAvailable,
47827
+ isTaskPriority: () => isTaskPriority,
47473
47828
  isTaskReadyForMerge: () => isTaskReadyForMerge,
47474
47829
  isValidPermission: () => isValidPermission,
47475
47830
  isValidPromptKey: () => isValidPromptKey,
@@ -47493,6 +47848,7 @@ __export(src_exports, {
47493
47848
  normalizePermissions: () => normalizePermissions,
47494
47849
  normalizeRoadmapFeatureOrder: () => normalizeRoadmapFeatureOrder,
47495
47850
  normalizeRoadmapMilestoneOrder: () => normalizeRoadmapMilestoneOrder,
47851
+ normalizeTaskPriority: () => normalizeTaskPriority,
47496
47852
  parseAgentManifest: () => parseAgentManifest,
47497
47853
  parseCompanyArchive: () => parseCompanyArchive,
47498
47854
  parseCompanyDirectory: () => parseCompanyDirectory,
@@ -47545,6 +47901,7 @@ __export(src_exports, {
47545
47901
  shouldSkipBackgroundQmdRefresh: () => shouldSkipBackgroundQmdRefresh,
47546
47902
  shouldTriggerExtraction: () => shouldTriggerExtraction,
47547
47903
  slugify: () => slugify,
47904
+ sortTasksByPriorityThenAgeAndId: () => sortTasksByPriorityThenAgeAndId,
47548
47905
  summarizeTitle: () => summarizeTitle,
47549
47906
  syncAutoSummarizeAutomation: () => syncAutoSummarizeAutomation,
47550
47907
  syncBackupAutomation: () => syncBackupAutomation,
@@ -47602,6 +47959,7 @@ var init_src = __esm({
47602
47959
  init_ai_summarize();
47603
47960
  init_memory_compaction();
47604
47961
  init_roadmap_ordering();
47962
+ init_task_priority();
47605
47963
  init_roadmap_handoff();
47606
47964
  init_mission_types();
47607
47965
  init_mission_store();
@@ -47628,24 +47986,49 @@ var init_src = __esm({
47628
47986
  });
47629
47987
 
47630
47988
  // ../engine/src/logger.js
47989
+ function withSeverityMarker2(level, payload) {
47990
+ return `${LOG_LEVEL_MARKER_PREFIX2}${level}${LOG_LEVEL_MARKER_SUFFIX2}${payload}`;
47991
+ }
47631
47992
  function createLogger2(prefix) {
47632
47993
  const tag = `[${prefix}]`;
47633
47994
  return {
47634
47995
  log(message, ...args) {
47635
- globalThis.console.error(`${tag} ${message}`, ...args);
47996
+ globalThis.console.error(withSeverityMarker2("info", `${tag} ${message}`), ...args);
47636
47997
  },
47637
47998
  warn(message, ...args) {
47638
- globalThis.console.warn(`${tag} ${message}`, ...args);
47999
+ globalThis.console.warn(withSeverityMarker2("warn", `${tag} ${message}`), ...args);
47639
48000
  },
47640
48001
  error(message, ...args) {
47641
- globalThis.console.error(`${tag} ${message}`, ...args);
48002
+ globalThis.console.error(withSeverityMarker2("error", `${tag} ${message}`), ...args);
47642
48003
  }
47643
48004
  };
47644
48005
  }
47645
- var schedulerLog, executorLog, triageLog, piLog, extensionsLog, mergerLog, worktreePoolLog, reviewerLog, prMonitorLog, runtimeLog, ipcLog, projectManagerLog, hybridExecutorLog, autopilotLog, heartbeatLog, remoteNodeLog, nodeHealthMonitorLog, peerExchangeLog;
48006
+ function formatError(err) {
48007
+ if (err instanceof Error) {
48008
+ const message2 = err.message || err.name || "Error";
48009
+ const stack = err.stack;
48010
+ const detail = stack && stack.includes(message2) ? stack : stack ? `${message2}
48011
+ ${stack}` : message2;
48012
+ return { message: message2, stack, detail };
48013
+ }
48014
+ let message;
48015
+ if (typeof err === "string") {
48016
+ message = err;
48017
+ } else {
48018
+ try {
48019
+ message = JSON.stringify(err);
48020
+ } catch {
48021
+ message = String(err);
48022
+ }
48023
+ }
48024
+ return { message, detail: message };
48025
+ }
48026
+ 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
48027
  var init_logger2 = __esm({
47647
48028
  "../engine/src/logger.js"() {
47648
48029
  "use strict";
48030
+ LOG_LEVEL_MARKER_PREFIX2 = "\0fnlvl=";
48031
+ LOG_LEVEL_MARKER_SUFFIX2 = "\0";
47649
48032
  schedulerLog = createLogger2("scheduler");
47650
48033
  executorLog = createLogger2("executor");
47651
48034
  triageLog = createLogger2("triage");
@@ -48100,7 +48483,7 @@ async function getAgentMemoryWindow(rootDir, agentMemory, path, startLine = 1, l
48100
48483
  }
48101
48484
  function createTaskCreateTool(store) {
48102
48485
  return {
48103
- name: "task_create",
48486
+ name: "fn_task_create",
48104
48487
  label: "Create Task",
48105
48488
  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
48489
  parameters: taskCreateParams,
@@ -48123,7 +48506,7 @@ function createTaskCreateTool(store) {
48123
48506
  }
48124
48507
  function createTaskLogTool(store, taskId) {
48125
48508
  return {
48126
- name: "task_log",
48509
+ name: "fn_task_log",
48127
48510
  label: "Log Entry",
48128
48511
  description: "Log an important action, decision, or issue for this task. Use for significant events \u2014 not every small step.",
48129
48512
  parameters: taskLogParams,
@@ -48138,7 +48521,7 @@ function createTaskLogTool(store, taskId) {
48138
48521
  }
48139
48522
  function createTaskLogToolWithContext(store, taskId, runContext) {
48140
48523
  return {
48141
- name: "task_log",
48524
+ name: "fn_task_log",
48142
48525
  label: "Log Entry",
48143
48526
  description: "Log an important action, decision, or issue for this task. Use for significant events \u2014 not every small step.",
48144
48527
  parameters: taskLogParams,
@@ -48153,7 +48536,7 @@ function createTaskLogToolWithContext(store, taskId, runContext) {
48153
48536
  }
48154
48537
  function createTaskDocumentWriteTool(store, taskId) {
48155
48538
  return {
48156
- name: "task_document_write",
48539
+ name: "fn_task_document_write",
48157
48540
  label: "Write Document",
48158
48541
  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
48542
  parameters: taskDocumentWriteParams,
@@ -48186,7 +48569,7 @@ function createTaskDocumentWriteTool(store, taskId) {
48186
48569
  }
48187
48570
  function createTaskDocumentReadTool(store, taskId) {
48188
48571
  return {
48189
- name: "task_document_read",
48572
+ name: "fn_task_document_read",
48190
48573
  label: "Read Document",
48191
48574
  description: "Read a named document for this task, or list all documents when no key is provided.",
48192
48575
  parameters: taskDocumentReadParams,
@@ -48242,9 +48625,9 @@ ${lines.join("\n")}`
48242
48625
  }
48243
48626
  function createMemorySearchTool(rootDir, settings, options) {
48244
48627
  return {
48245
- name: "memory_search",
48628
+ name: "fn_memory_search",
48246
48629
  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.",
48630
+ 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
48631
  parameters: memorySearchParams,
48249
48632
  execute: async (_id, params) => {
48250
48633
  const limit = params.limit ?? 5;
@@ -48270,9 +48653,9 @@ function createMemorySearchTool(rootDir, settings, options) {
48270
48653
  }
48271
48654
  function createMemoryGetTool(rootDir, settings, options) {
48272
48655
  return {
48273
- name: "memory_get",
48656
+ name: "fn_memory_get",
48274
48657
  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.",
48658
+ 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
48659
  parameters: memoryGetParams,
48277
48660
  execute: async (_id, params) => {
48278
48661
  const agentResult = options?.agentMemory ? await getAgentMemoryWindow(rootDir, options.agentMemory, params.path, params.startLine, params.lineCount) : null;
@@ -48306,7 +48689,7 @@ ${result.content}`
48306
48689
  }
48307
48690
  function createMemoryAppendTool(rootDir, settings, options) {
48308
48691
  return {
48309
- name: "memory_append",
48692
+ name: "fn_memory_append",
48310
48693
  label: "Append Memory",
48311
48694
  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
48695
  parameters: memoryAppendParams,
@@ -48368,7 +48751,7 @@ function createMemoryTools(rootDir, settings, options) {
48368
48751
  }
48369
48752
  function createReflectOnPerformanceTool(reflectionService, agentId) {
48370
48753
  return {
48371
- name: "reflect_on_performance",
48754
+ name: "fn_reflect_on_performance",
48372
48755
  label: "Reflect on Performance",
48373
48756
  description: 'Review your past task performance and generate insights for improvement. Optionally focus on a specific area like "code quality", "speed", or "testing".',
48374
48757
  parameters: reflectOnPerformanceParams,
@@ -48401,7 +48784,7 @@ function createReflectOnPerformanceTool(reflectionService, agentId) {
48401
48784
  }
48402
48785
  function createListAgentsTool(agentStore) {
48403
48786
  return {
48404
- name: "list_agents",
48787
+ name: "fn_list_agents",
48405
48788
  label: "List Agents",
48406
48789
  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
48790
  parameters: listAgentsParams,
@@ -48444,9 +48827,9 @@ ${lines.join("\n\n")}` }],
48444
48827
  }
48445
48828
  function createDelegateTaskTool(agentStore, taskStore) {
48446
48829
  return {
48447
- name: "delegate_task",
48830
+ name: "fn_delegate_task",
48448
48831
  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.",
48832
+ 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
48833
  parameters: delegateTaskParams,
48451
48834
  execute: async (_id, params) => {
48452
48835
  const agent = await agentStore.getAgent(params.agent_id);
@@ -48481,7 +48864,7 @@ function createDelegateTaskTool(agentStore, taskStore) {
48481
48864
  }
48482
48865
  function createSendMessageTool(messageStore, fromAgentId) {
48483
48866
  return {
48484
- name: "send_message",
48867
+ name: "fn_send_message",
48485
48868
  label: "Send Message",
48486
48869
  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
48870
  parameters: sendMessageParams,
@@ -48538,7 +48921,7 @@ function createSendMessageTool(messageStore, fromAgentId) {
48538
48921
  }
48539
48922
  function createReadMessagesTool(messageStore, agentId) {
48540
48923
  return {
48541
- name: "read_messages",
48924
+ name: "fn_read_messages",
48542
48925
  label: "Read Messages",
48543
48926
  description: "Read your inbox messages. Returns unread messages by default.",
48544
48927
  parameters: readMessagesParams,
@@ -48640,7 +49023,7 @@ var init_agent_tools = __esm({
48640
49023
  Type.Literal("agent-to-user")
48641
49024
  ], { description: "Message type (defaults to 'agent-to-agent')" })),
48642
49025
  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)" })
49026
+ Type.String({ description: "Optional ID of the message you are replying to (use IDs from fn_read_messages output)" })
48644
49027
  )
48645
49028
  });
48646
49029
  readMessagesParams = Type.Object({
@@ -48652,7 +49035,7 @@ var init_agent_tools = __esm({
48652
49035
  limit: Type.Optional(Type.Number({ description: "Maximum snippets to return (default: 5, max: 20)" }))
48653
49036
  });
48654
49037
  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" }),
49038
+ path: Type.String({ description: "Memory path from fn_memory_search, e.g. .fusion/memory/MEMORY.md or .fusion/memory/YYYY-MM-DD.md" }),
48656
49039
  startLine: Type.Optional(Type.Number({ description: "1-based start line (default: 1)" })),
48657
49040
  lineCount: Type.Optional(Type.Number({ description: "Number of lines to read (default: 120, max: 400)" }))
48658
49041
  });
@@ -49156,7 +49539,7 @@ __export(pi_exports, {
49156
49539
  import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
49157
49540
  import { exec } from "node:child_process";
49158
49541
  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";
49542
+ import { basename as basename7, dirname as dirname7, join as join24, relative as relative3, isAbsolute as isAbsolute6, resolve as resolve10 } from "node:path";
49160
49543
  import { createAgentSession, createCodingTools, createExtensionRuntime, createReadOnlyTools, DefaultResourceLoader, DefaultPackageManager, discoverAndLoadExtensions, ModelRegistry, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
49161
49544
  function getSessionStateError(session) {
49162
49545
  const error = session.state?.error;
@@ -49480,10 +49863,10 @@ function hasPackageManagerSettings(settings) {
49480
49863
  return Array.isArray(settings.packages) || Array.isArray(settings.npmCommand);
49481
49864
  }
49482
49865
  function siblingAgentDir(agentDir, siblingRoot) {
49483
- if (basename6(agentDir) !== "agent") {
49866
+ if (basename7(agentDir) !== "agent") {
49484
49867
  return void 0;
49485
49868
  }
49486
- return join24(dirname6(dirname6(agentDir)), siblingRoot, "agent");
49869
+ return join24(dirname7(dirname7(agentDir)), siblingRoot, "agent");
49487
49870
  }
49488
49871
  function createReadOnlyPiSettingsView(cwd, agentDir) {
49489
49872
  const projectRoot = resolvePiExtensionProjectRoot(cwd);
@@ -50802,12 +51185,12 @@ When reviewing specs, actively assess whether the task should have been broken i
50802
51185
  Say explicitly: "This task should be broken into subtasks because [specific reason]."
50803
51186
  Recommend the number of child tasks (2-5) and what each should cover.
50804
51187
  **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
51188
+ 1. Use the \`fn_task_create\` tool to create 2\u20135 child tasks from the oversized spec
50806
51189
  2. Do NOT write a parent PROMPT.md \u2014 the parent will be closed automatically after children are created
50807
51190
  3. Each child task should cover one coherent deliverable with clear scope boundaries
50808
51191
 
50809
51192
  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."
51193
+ "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
51194
 
50812
51195
  **Do NOT flag if:**
50813
51196
  - Steps are sequential and tightly coupled (e.g., a pipeline where each step depends on the previous)
@@ -51180,12 +51563,12 @@ The user has requested that this task be broken into smaller subtasks if it is c
51180
51563
 
51181
51564
  **How to split:**
51182
51565
  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
51566
+ 2. If splitting: use the \\\`fn_task_create\\\` tool to create child tasks in order, setting up dependencies as needed
51184
51567
  3. Include clear descriptions and acceptance criteria for each child task
51185
51568
  4. After creating all subtasks, stop \u2014 do NOT write a PROMPT.md for the parent task
51186
51569
  5. If NOT splitting: proceed with a normal PROMPT.md specification for this task
51187
51570
 
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.
51571
+ **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
51572
 
51190
51573
  **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
51574
  } else {
@@ -51211,7 +51594,7 @@ The user did not explicitly request subtask breakdown, so you should first asses
51211
51594
  - Adding a small feature to one module with 5 steps
51212
51595
 
51213
51596
  **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.
51597
+ - 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
51598
  - **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
51599
  - 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
51600
  - If size is uncertain at first, make a quick assessment from the available context before deciding.`;
@@ -51325,8 +51708,8 @@ Follow this structure exactly:
51325
51708
  ### Step {N}: Documentation & Delivery
51326
51709
 
51327
51710
  - [ ] 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
51711
+ - [ ] Save documentation deliverables as task documents via \`fn_task_document_write\` (key="docs", content=...)
51712
+ - [ ] Out-of-scope findings created as new tasks via \`fn_task_create\` tool
51330
51713
 
51331
51714
  ## Documentation Requirements
51332
51715
 
@@ -51359,7 +51742,7 @@ Commits at step boundaries. All commits include the task ID:
51359
51742
  - Refuse necessary fixes just because they touch files outside the initial File Scope
51360
51743
  - Commit without the task ID prefix
51361
51744
  - 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\`
51745
+ - Remove features as "cleanup" \u2014 if something seems unused, create a task via \`fn_task_create\`
51363
51746
 
51364
51747
  ## Changeset Requirements
51365
51748
 
@@ -51381,13 +51764,13 @@ tests. Manual verification is NOT a test.
51381
51764
  as part of this task (not just skipping tests)
51382
51765
 
51383
51766
  ## Duplicate check
51384
- Before writing a spec, call \`task_list\` to see existing tasks.
51767
+ Before writing a spec, call \`fn_task_list\` to see existing tasks.
51385
51768
  If a task already covers the same work (even if worded differently), do NOT
51386
51769
  write a PROMPT.md. Instead, write a single line to the output file:
51387
51770
  \`DUPLICATE: {existing-task-id}\`
51388
51771
 
51389
51772
  ## 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.
51773
+ 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
51774
  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
51775
  If the dependency task has no PROMPT.md yet (not yet specified), note that in the Dependencies section.
51393
51776
 
@@ -51395,8 +51778,8 @@ If the dependency task has no PROMPT.md yet (not yet specified), note that in th
51395
51778
  When the task includes \`breakIntoSubtasks: true\`, first decide whether it should be split.
51396
51779
 
51397
51780
  - 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.
51781
+ - 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.
51782
+ - **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
51783
  - If not splitting: proceed with a normal PROMPT.md specification.
51401
51784
 
51402
51785
  ## Proactive Subtask Breakdown for M/L Tasks
@@ -51419,20 +51802,20 @@ For tasks you assess as Size M or L, proactively evaluate whether splitting into
51419
51802
 
51420
51803
  ## Triage tools
51421
51804
  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
51805
+ - \`fn_task_list\` \u2014 list existing active tasks
51806
+ - \`fn_task_get\` \u2014 inspect a task and its PROMPT.md
51807
+ - \`fn_task_create\` \u2014 create a child/follow-up task while triaging
51808
+ - \`fn_task_document_write\` \u2014 save a planning document (e.g., key="plan")
51809
+ - \`fn_task_document_read\` \u2014 read back a previously saved document
51427
51810
 
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.
51811
+ 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
51812
 
51430
51813
  ## Guidelines
51431
51814
  - Read the project structure and relevant source files to understand context BEFORE writing
51432
51815
  - Be specific \u2014 name actual files, functions, and patterns from the codebase
51433
51816
  - Steps should express OUTCOMES, not micro-instructions (2-5 checkboxes per step)
51434
51817
  - 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\`
51818
+ - 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
51819
  - Include a "Do NOT" section with project-appropriate guardrails
51437
51820
  - Size assessment: S (<2h), M (2-4h), L (4-8h). Split if XL (8h+)
51438
51821
  - Review level scoring: Blast radius (0-2), Pattern novelty (0-2), Security (0-2), Reversibility (0-2)
@@ -51446,16 +51829,16 @@ package.json when explicit commands are provided.
51446
51829
 
51447
51830
  ## Spec Review
51448
51831
 
51449
- After writing the PROMPT.md, call \`review_spec()\` to get an independent quality review.
51832
+ After writing the PROMPT.md, call \`fn_review_spec()\` to get an independent quality review.
51450
51833
 
51451
51834
  - **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.
51835
+ - **REVISE** \u2192 fix the issues described in the review feedback, rewrite the PROMPT.md, and call \`fn_review_spec()\` again. Repeat until approved.
51453
51836
  - **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
51837
 
51455
- You MUST call \`review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
51838
+ You MUST call \`fn_review_spec()\` after writing the PROMPT.md. Do not finish without getting an APPROVE verdict.
51456
51839
 
51457
51840
  ## Output
51458
- Write the PROMPT.md directly using the write tool, then call \`review_spec()\` for review.
51841
+ Write the PROMPT.md directly using the write tool, then call \`fn_review_spec()\` for review.
51459
51842
 
51460
51843
  ## Frontend UX Criteria Injection
51461
51844
 
@@ -51702,9 +52085,10 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
51702
52085
  this.wasEnginePaused = false;
51703
52086
  const allTasks = await this.store.listTasks({ slim: true, includeArchived: false });
51704
52087
  const now = Date.now();
51705
- const triageTasks = allTasks.filter(
52088
+ const eligibleTriageTasks = allTasks.filter(
51706
52089
  (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
52090
  );
52091
+ const triageTasks = sortTasksByPriorityThenAgeAndId(eligibleTriageTasks);
51708
52092
  const maxTriageConcurrent = settings.maxTriageConcurrent ?? settings.maxConcurrent ?? 2;
51709
52093
  const specifying = allTasks.filter(
51710
52094
  (t) => t.column === "triage" && t.status === "specifying" && !t.paused
@@ -51730,11 +52114,11 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
51730
52114
  /**
51731
52115
  * Specify a triage task by spawning an AI agent to generate a PROMPT.md.
51732
52116
  *
51733
- * After the agent writes the PROMPT.md, it calls `review_spec()` to spawn
52117
+ * After the agent writes the PROMPT.md, it calls `fn_review_spec()` to spawn
51734
52118
  * an independent reviewer agent that evaluates the specification quality.
51735
52119
  * The review loop works as follows:
51736
52120
  * - **APPROVE**: the spec is accepted and the task moves to `todo`
51737
- * - **REVISE**: the agent revises the spec and calls `review_spec()` again.
52121
+ * - **REVISE**: the agent revises the spec and calls `fn_review_spec()` again.
51738
52122
  * If the agent finishes without getting APPROVE, the task is NOT moved to
51739
52123
  * `todo` — a post-session gate requires an explicit APPROVE verdict.
51740
52124
  * - **RETHINK**: the conversation rewinds to a pre-specification checkpoint
@@ -51938,7 +52322,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
51938
52322
  const planningFallbackModelId = settings.planningFallbackModelId;
51939
52323
  const canRetryWithPlanningFallback = specReviewVerdictRef.current !== "APPROVE" && planningFallbackProvider && planningFallbackModelId && modelDesc !== `${planningFallbackProvider}/${planningFallbackModelId}`;
51940
52324
  if (canRetryWithPlanningFallback) {
51941
- const verdictDesc = specReviewVerdictRef.current === null ? "review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
52325
+ const verdictDesc = specReviewVerdictRef.current === null ? "fn_review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
51942
52326
  const fallbackDesc = `${planningFallbackProvider}/${planningFallbackModelId}`;
51943
52327
  triageLog.warn(
51944
52328
  `${task.id} primary planning model produced no approved spec (${verdictDesc}) \u2014 retrying with fallback ${fallbackDesc}`
@@ -52001,7 +52385,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52001
52385
  }
52002
52386
  }
52003
52387
  if (specReviewVerdictRef.current !== "APPROVE") {
52004
- const verdictDesc = specReviewVerdictRef.current === null ? "review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
52388
+ const verdictDesc = specReviewVerdictRef.current === null ? "fn_review_spec was never called" : `verdict was ${specReviewVerdictRef.current}`;
52005
52389
  const decision = computeRecoveryDecision({
52006
52390
  recoveryRetryCount: task.recoveryRetryCount,
52007
52391
  nextRecoveryAt: task.nextRecoveryAt
@@ -52086,7 +52470,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52086
52470
  await retryableWork();
52087
52471
  }
52088
52472
  } catch (err) {
52089
- const errorMessage = err instanceof Error ? err.message : String(err);
52473
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
52090
52474
  if (err.code === "ENOENT") {
52091
52475
  triageLog.log(`${task.id} no longer exists \u2014 skipping`);
52092
52476
  } else if (this.pauseAborted.has(task.id)) {
@@ -52159,7 +52543,13 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52159
52543
  const msg = restoreErr instanceof Error ? restoreErr.message : String(restoreErr);
52160
52544
  triageLog.warn(`${task.id}: failed to restore status to '${restoreStatus}' after specification error: ${msg}`);
52161
52545
  });
52162
- triageLog.error(`\u2717 ${task.id} specification failed:`, errorMessage);
52546
+ triageLog.error(`\u2717 ${task.id} specification failed:`, errorDetail);
52547
+ if (errorStack) {
52548
+ await this.store.logEntry(task.id, `Specification failed: ${errorMessage}`, errorStack).catch((logErr) => {
52549
+ const msg = logErr instanceof Error ? logErr.message : String(logErr);
52550
+ triageLog.warn(`${task.id}: failed to persist specification-failure stack trace: ${msg}`);
52551
+ });
52552
+ }
52163
52553
  this.options.onSpecifyError?.(task, err instanceof Error ? err : new Error(errorMessage));
52164
52554
  }
52165
52555
  } finally {
@@ -52180,7 +52570,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52180
52570
  )
52181
52571
  });
52182
52572
  const taskList = {
52183
- name: "task_list",
52573
+ name: "fn_task_list",
52184
52574
  label: "List Tasks",
52185
52575
  description: "List all tasks that aren't done. Returns ID, description, column, and dependencies for each. Use to check for duplicates before specifying.",
52186
52576
  parameters: Type2.Object({}),
@@ -52205,7 +52595,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52205
52595
  }
52206
52596
  };
52207
52597
  const taskGet = {
52208
- name: "task_get",
52598
+ name: "fn_task_get",
52209
52599
  label: "Get Task",
52210
52600
  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
52601
  parameters: taskGetParams,
@@ -52227,7 +52617,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52227
52617
  };
52228
52618
  } catch (err) {
52229
52619
  const msg = err instanceof Error ? err.message : String(err);
52230
- triageLog.warn(`${options.parentTaskId}: task_get lookup failed for ${params.id}: ${msg}`);
52620
+ triageLog.warn(`${options.parentTaskId}: fn_task_get lookup failed for ${params.id}: ${msg}`);
52231
52621
  return {
52232
52622
  content: [
52233
52623
  { type: "text", text: `Task ${params.id} not found.` }
@@ -52238,7 +52628,7 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52238
52628
  }
52239
52629
  };
52240
52630
  const taskCreate = {
52241
- name: "task_create",
52631
+ name: "fn_task_create",
52242
52632
  label: "Create Child Task",
52243
52633
  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
52634
  parameters: taskCreateParams3,
@@ -52276,10 +52666,10 @@ Only inject this section when the task genuinely touches frontend UI. Omit it fo
52276
52666
  content: [
52277
52667
  {
52278
52668
  type: "text",
52279
- text: `ERROR: task_create rejected. Invalid dependencies:
52669
+ text: `ERROR: fn_task_create rejected. Invalid dependencies:
52280
52670
  ${summary}
52281
52671
 
52282
- Remove or replace these ids and call task_create again.`
52672
+ Remove or replace these ids and call fn_task_create again.`
52283
52673
  }
52284
52674
  ],
52285
52675
  details: { rejectedDependencies: rejected }
@@ -52290,7 +52680,7 @@ Remove or replace these ids and call task_create again.`
52290
52680
  parentTask = await store.getTask(options.parentTaskId);
52291
52681
  } catch (err) {
52292
52682
  const msg = err instanceof Error ? err.message : String(err);
52293
- triageLog.warn(`${options.parentTaskId}: failed to load parent task for task_create inheritance: ${msg}`);
52683
+ triageLog.warn(`${options.parentTaskId}: failed to load parent task for fn_task_create inheritance: ${msg}`);
52294
52684
  parentTask = void 0;
52295
52685
  }
52296
52686
  const newTask = await store.createTask({
@@ -52331,13 +52721,13 @@ Remove or replace these ids and call task_create again.`
52331
52721
  return [taskList, taskGet, taskCreate];
52332
52722
  }
52333
52723
  /**
52334
- * Create the `review_spec` tool for the triage agent.
52724
+ * Create the `fn_review_spec` tool for the triage agent.
52335
52725
  *
52336
52726
  * Spawns an independent reviewer agent to evaluate the generated PROMPT.md.
52337
52727
  * Verdict handling:
52338
52728
  * - **APPROVE**: returns "APPROVE" — the triage agent's work is done.
52339
52729
  * - **REVISE**: returns the review feedback. The triage agent must fix the
52340
- * PROMPT.md and call `review_spec` again. A post-session gate in
52730
+ * PROMPT.md and call `fn_review_spec` again. A post-session gate in
52341
52731
  * `specifyTask()` prevents moving to `todo` if the last verdict is REVISE.
52342
52732
  * - **RETHINK**: rewinds the conversation to a pre-specification checkpoint
52343
52733
  * using `session.navigateTree()`. Returns a re-prompt instructing the agent
@@ -52348,7 +52738,7 @@ Remove or replace these ids and call task_create again.`
52348
52738
  const rootDir = this.rootDir;
52349
52739
  const options = this.options;
52350
52740
  return {
52351
- name: "review_spec",
52741
+ name: "fn_review_spec",
52352
52742
  label: "Review Specification",
52353
52743
  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
52744
  parameters: Type2.Object({}),
@@ -52366,7 +52756,7 @@ Remove or replace these ids and call task_create again.`
52366
52756
  "utf-8"
52367
52757
  ).catch((err) => {
52368
52758
  const msg = err instanceof Error ? err.message : String(err);
52369
- triageLog.warn(`${taskId}: failed to read PROMPT.md for review_spec (${promptPath}): ${msg}`);
52759
+ triageLog.warn(`${taskId}: failed to read PROMPT.md for fn_review_spec (${promptPath}): ${msg}`);
52370
52760
  return "";
52371
52761
  });
52372
52762
  if (!promptContent) {
@@ -52374,7 +52764,7 @@ Remove or replace these ids and call task_create again.`
52374
52764
  content: [
52375
52765
  {
52376
52766
  type: "text",
52377
- text: "UNAVAILABLE \u2014 PROMPT.md file not found or empty. Write the specification first, then call review_spec."
52767
+ text: "UNAVAILABLE \u2014 PROMPT.md file not found or empty. Write the specification first, then call fn_review_spec."
52378
52768
  }
52379
52769
  ],
52380
52770
  details: {}
@@ -52430,7 +52820,7 @@ Remove or replace these ids and call task_create again.`
52430
52820
  text = "APPROVE";
52431
52821
  break;
52432
52822
  case "REVISE":
52433
- text = `REVISE \u2014 fix the issues below, rewrite the PROMPT.md, and call review_spec() again.
52823
+ text = `REVISE \u2014 fix the issues below, rewrite the PROMPT.md, and call fn_review_spec() again.
52434
52824
 
52435
52825
  ${result.review}`;
52436
52826
  break;
@@ -52854,6 +53244,11 @@ function inferDefaultTestCommand(rootDir, explicitTestCommand, explicitBuildComm
52854
53244
  };
52855
53245
  }
52856
53246
  if (existsSync21(join26(rootDir, "pnpm-lock.yaml"))) {
53247
+ if (existsSync21(join26(rootDir, "pnpm-workspace.yaml"))) {
53248
+ mergerLog.warn(
53249
+ `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\`.`
53250
+ );
53251
+ }
52857
53252
  return {
52858
53253
  command: "pnpm test",
52859
53254
  testSource: "inferred",
@@ -52962,6 +53357,7 @@ async function runVerificationCommand(store, rootDir, taskId, command, type) {
52962
53357
  stderr: "",
52963
53358
  success: false
52964
53359
  };
53360
+ const verificationStartedAt = Date.now();
52965
53361
  try {
52966
53362
  const { stdout, stderr } = await execAsync2(command, {
52967
53363
  cwd: rootDir,
@@ -52973,37 +53369,46 @@ async function runVerificationCommand(store, rootDir, taskId, command, type) {
52973
53369
  result.stderr = stderr?.toString?.() || "";
52974
53370
  result.exitCode = 0;
52975
53371
  result.success = true;
52976
- mergerLog.log(`${taskId}: ${type} command succeeded`);
52977
- await store.logEntry(taskId, `[verification] ${type} command succeeded (exit 0)`);
53372
+ const verificationDurationMs = Date.now() - verificationStartedAt;
53373
+ mergerLog.log(`${taskId}: ${type} command succeeded in ${verificationDurationMs}ms`);
53374
+ await store.logEntry(taskId, `[timing] [verification] ${type} command succeeded (exit 0) in ${verificationDurationMs}ms`);
52978
53375
  return result;
52979
53376
  } catch (error) {
53377
+ const verificationDurationMs = Date.now() - verificationStartedAt;
52980
53378
  result.stdout = error?.stdout?.toString?.() || "";
52981
53379
  result.stderr = error?.stderr?.toString?.() || "";
52982
53380
  result.exitCode = typeof error?.status === "number" ? error.status : typeof error?.code === "number" ? error.code : null;
52983
53381
  const maxBufferExceeded = error?.code === "ENOBUFS" || error?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || String(error?.message ?? "").includes("maxBuffer");
52984
53382
  result.success = maxBufferExceeded && result.exitCode === 0;
52985
53383
  if (result.success) {
52986
- mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer)`);
53384
+ mergerLog.log(`${taskId}: ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`);
52987
53385
  await store.logEntry(
52988
53386
  taskId,
52989
- `[verification] ${type} command succeeded (exit 0, output exceeded buffer)`
53387
+ `[timing] [verification] ${type} command succeeded (exit 0, output exceeded buffer) in ${verificationDurationMs}ms`
52990
53388
  );
52991
53389
  return result;
52992
53390
  }
52993
53391
  const output = result.stderr || result.stdout || error?.message || "Unknown error";
52994
53392
  const summary = summarizeVerificationOutput(output, type);
52995
- mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}); output captured in task log`);
53393
+ mergerLog.error(`${taskId}: ${type} command failed (exit ${result.exitCode}) in ${verificationDurationMs}ms; output captured in task log`);
52996
53394
  await store.logEntry(
52997
53395
  taskId,
52998
- `[verification] ${type} command failed (exit ${result.exitCode}):
53396
+ `[timing] [verification] ${type} command failed (exit ${result.exitCode}) after ${verificationDurationMs}ms:
52999
53397
  ${summary}`
53000
53398
  );
53001
53399
  }
53002
53400
  return result;
53003
53401
  }
53004
- async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, _testCommand, _buildCommand) {
53402
+ async function attemptInMergeVerificationFix(store, rootDir, taskId, failureContext, settings, options, mergeRunContext, fixAttemptNumber, _testCommand, _buildCommand) {
53005
53403
  try {
53006
53404
  mergerLog.log(`${taskId}: spawning in-merge verification fix agent`);
53405
+ const logger2 = new AgentLogger({
53406
+ store,
53407
+ taskId,
53408
+ agent: "merger",
53409
+ onAgentText: options.onAgentText,
53410
+ onAgentTool: options.onAgentTool
53411
+ });
53007
53412
  let skillContext = void 0;
53008
53413
  if (options.agentStore) {
53009
53414
  try {
@@ -53035,12 +53440,22 @@ A merge has been applied and the verification command failed. Your job is to fix
53035
53440
  6. If you cannot fix the issue, explain why`,
53036
53441
  tools: "coding",
53037
53442
  // Agent needs read/write file access
53443
+ onText: logger2.onText,
53444
+ onThinking: logger2.onThinking,
53445
+ onToolStart: logger2.onToolStart,
53446
+ onToolEnd: logger2.onToolEnd,
53038
53447
  defaultProvider: settings.defaultProvider,
53039
53448
  defaultModelId: settings.defaultModelId,
53040
53449
  defaultThinkingLevel: settings.defaultThinkingLevel,
53041
53450
  // Skill selection: use assigned agent skills if available, otherwise role fallback
53042
53451
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
53043
53452
  });
53453
+ const runId = mergeRunContext?.runId;
53454
+ const agentId = mergeRunContext?.agentId ?? "merger";
53455
+ await store.logEntry(
53456
+ taskId,
53457
+ `In-merge verification fix agent started (model: ${describeModel(session)}, runId: ${runId ?? "unknown"}, agentId: ${agentId})`
53458
+ );
53044
53459
  try {
53045
53460
  const fixPrompt = `Fix the failing ${failureContext.type} verification for task ${taskId}.
53046
53461
 
@@ -53065,6 +53480,10 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
53065
53480
  mergerLog.warn(`\u23F3 ${taskId} in-merge fix rate limited \u2014 retry ${attempt} in ${delaySec}s: ${error.message}`);
53066
53481
  }
53067
53482
  });
53483
+ await store.logEntry(
53484
+ taskId,
53485
+ `Re-running deterministic merge verification (attempt ${fixAttemptNumber ?? "unknown"})`
53486
+ );
53068
53487
  const reRunResult = await runVerificationCommand(
53069
53488
  store,
53070
53489
  rootDir,
@@ -53074,6 +53493,7 @@ ${failureContext.output.slice(0, VERIFICATION_LOG_MAX_CHARS)}
53074
53493
  );
53075
53494
  return reRunResult.success;
53076
53495
  } finally {
53496
+ await logger2.flush();
53077
53497
  await session.dispose();
53078
53498
  }
53079
53499
  } catch (err) {
@@ -53365,7 +53785,7 @@ and the bash tool returned exit code 0.
53365
53785
  1. Run the build command (shown in the prompt context below)
53366
53786
  2. If the build succeeds (exit code 0), proceed with the commit
53367
53787
  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
53788
+ - Call the \`fn_report_build_failure\` tool with the real error details
53369
53789
  - Stop immediately and do not run \`git commit\`
53370
53790
  - Do not claim success in plain text
53371
53791
 
@@ -53404,7 +53824,7 @@ and the bash tool returned exit code 0.
53404
53824
  1. Run the build command (shown in the prompt context below)
53405
53825
  2. If the build succeeds (exit code 0), proceed with the commit
53406
53826
  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
53827
+ - Call the \`fn_report_build_failure\` tool with the real error details
53408
53828
  - Stop immediately and do not run \`git commit\`
53409
53829
  - Do not claim success in plain text
53410
53830
 
@@ -53947,6 +54367,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53947
54367
  if (failedResult) {
53948
54368
  let fixSuccess = false;
53949
54369
  for (let fixAttempt = 1; fixAttempt <= maxFixRetries; fixAttempt++) {
54370
+ const fixAttemptStartedAt = Date.now();
53950
54371
  mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53951
54372
  await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53952
54373
  fixSuccess = await attemptInMergeVerificationFix(
@@ -53961,16 +54382,19 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53961
54382
  },
53962
54383
  settings,
53963
54384
  options,
54385
+ { runId: mergeRunId, agentId: engineRunContext.agentId },
54386
+ fixAttempt,
53964
54387
  effectiveTestCommand,
53965
54388
  effectiveBuildCommand
53966
54389
  );
54390
+ const fixAttemptDurationMs = Date.now() - fixAttemptStartedAt;
53967
54391
  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`);
54392
+ mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
54393
+ await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms \u2014 verification now passes`);
53970
54394
  break;
53971
54395
  }
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`);
54396
+ mergerLog.warn(`${taskId}: in-merge verification fix attempt ${fixAttempt} \u2014 verification still fails (${fixAttemptDurationMs}ms)`);
54397
+ await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} \u2014 verification still fails (${fixAttemptDurationMs}ms)`);
53974
54398
  }
53975
54399
  if (fixSuccess) {
53976
54400
  const authorArg = getCommitAuthorArg(settings);
@@ -53991,6 +54415,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53991
54415
  const fixType = effectiveBuildCommand ? "build" : "test";
53992
54416
  let fixSuccess = false;
53993
54417
  for (let fixAttempt = 1; fixAttempt <= maxFixRetries; fixAttempt++) {
54418
+ const fixAttemptStartedAt = Date.now();
53994
54419
  mergerLog.log(`${taskId}: in-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53995
54420
  await store.logEntry(taskId, `In-merge verification fix attempt ${fixAttempt}/${maxFixRetries}`);
53996
54421
  fixSuccess = await attemptInMergeVerificationFix(
@@ -54005,14 +54430,18 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
54005
54430
  },
54006
54431
  settings,
54007
54432
  options,
54433
+ { runId: mergeRunId, agentId: engineRunContext.agentId },
54434
+ fixAttempt,
54008
54435
  effectiveTestCommand,
54009
54436
  effectiveBuildCommand
54010
54437
  );
54438
+ const fixAttemptDurationMs = Date.now() - fixAttemptStartedAt;
54011
54439
  if (fixSuccess) {
54012
- mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt}`);
54013
- await store.logEntry(taskId, `In-merge verification fix succeeded`);
54440
+ mergerLog.log(`${taskId}: in-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
54441
+ await store.logEntry(taskId, `[timing] In-merge verification fix succeeded on attempt ${fixAttempt} in ${fixAttemptDurationMs}ms`);
54014
54442
  break;
54015
54443
  }
54444
+ await store.logEntry(taskId, `[timing] In-merge verification fix attempt ${fixAttempt} \u2014 verification still fails (${fixAttemptDurationMs}ms)`);
54016
54445
  }
54017
54446
  if (fixSuccess) {
54018
54447
  const authorArg = getCommitAuthorArg(settings);
@@ -54091,8 +54520,15 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
54091
54520
  deletions = deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0;
54092
54521
  } catch {
54093
54522
  }
54523
+ const isEmptyCommit = filesChanged === 0;
54524
+ const recordedSha = isEmptyCommit ? void 0 : commitSha;
54525
+ if (isEmptyCommit) {
54526
+ mergerLog.warn(
54527
+ `${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.`
54528
+ );
54529
+ }
54094
54530
  const mergeDetails = {
54095
- commitSha,
54531
+ commitSha: recordedSha,
54096
54532
  filesChanged,
54097
54533
  insertions,
54098
54534
  deletions,
@@ -54105,7 +54541,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
54105
54541
  autoResolvedCount: result.autoResolvedCount
54106
54542
  };
54107
54543
  await store.updateTask(taskId, { mergeDetails });
54108
- mergerLog.log(`${taskId}: merge details stored (commitSha: ${commitSha?.slice(0, 8)})`);
54544
+ mergerLog.log(`${taskId}: merge details stored (commitSha: ${recordedSha?.slice(0, 8) ?? "<deferred>"})`);
54109
54545
  } catch (err) {
54110
54546
  mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
54111
54547
  }
@@ -54461,7 +54897,7 @@ async function runAiAgentForCommit(params) {
54461
54897
  let buildFailed = false;
54462
54898
  let buildErrorMessage = "";
54463
54899
  const reportBuildFailureTool = {
54464
- name: "report_build_failure",
54900
+ name: "fn_report_build_failure",
54465
54901
  label: "Report Build Failure",
54466
54902
  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
54903
  parameters: Type3.Object({
@@ -54678,7 +55114,7 @@ function buildMergePrompt(params) {
54678
55114
  "This command is mandatory before commit.",
54679
55115
  "Run it with the bash tool in the current worktree and inspect the actual exit code.",
54680
55116
  "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."
55117
+ "If it exits non-zero, call `fn_report_build_failure` with the concrete error output and stop without committing."
54682
55118
  );
54683
55119
  }
54684
55120
  if (buildCommand) {
@@ -54690,7 +55126,7 @@ function buildMergePrompt(params) {
54690
55126
  "This command is mandatory before commit.",
54691
55127
  "Run it with the bash tool in the current worktree and inspect the actual exit code.",
54692
55128
  "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."
55129
+ "If it exits non-zero, call `fn_report_build_failure` with the concrete error output and stop without committing."
54694
55130
  );
54695
55131
  }
54696
55132
  return parts.join("\n");
@@ -55719,11 +56155,11 @@ function buildStepPrompt(taskDetail, stepIndex, rootDir, settings, worktreePath)
55719
56155
  if (isLastStep) {
55720
56156
  parts.push(
55721
56157
  "",
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.`,
56158
+ `**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
56159
  ""
55724
56160
  );
55725
56161
  }
55726
- parts.push("After completing this step, commit your changes and call task_done(). Do NOT proceed to subsequent steps.");
56162
+ parts.push("After completing this step, commit your changes and call fn_task_done(). Do NOT proceed to subsequent steps.");
55727
56163
  return parts.join("\n");
55728
56164
  }
55729
56165
  function scopePromptToWorktree(prompt, rootDir, worktreePath) {
@@ -55778,7 +56214,7 @@ function buildReducedStepPrompt(taskDetail, stepIndex) {
55778
56214
  "IMPORTANT: Your previous attempt hit the context window limit.",
55779
56215
  "Do NOT repeat work that's already been done.",
55780
56216
  "Check git status and git log to see what's been committed.",
55781
- "Complete the remaining work and call task_done()."
56217
+ "Complete the remaining work and call fn_task_done()."
55782
56218
  ];
55783
56219
  return parts.join("\n").replace(/\n{3,}/g, "\n\n");
55784
56220
  }
@@ -56578,10 +57014,10 @@ ${attachmentsSection}${commandsSection}${memorySection}${progressSection}${steer
56578
57014
 
56579
57015
  ${reviewLevel === 0 ? "No reviews required. Implement directly." : ""}
56580
57016
  ${reviewLevel >= 1 ? `Before implementing each step (except Step 0 and the final step), call:
56581
- \`review_step(step=N, type="plan", step_name="...")\`` : ""}
57017
+ \`fn_review_step(step=N, type="plan", step_name="...")\`` : ""}
56582
57018
  ${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.` : ""}
57019
+ \`fn_review_step(step=N, type="code", step_name="...", baseline="<SHA from before step>")\`` : ""}
57020
+ ${reviewLevel >= 3 ? `After tests, also call fn_review_step with type="code" for test review.` : ""}
56585
57021
 
56586
57022
  ## Worktree Boundaries
56587
57023
 
@@ -56590,23 +57026,24 @@ You are running in an **isolated git worktree**. This means:
56590
57026
  - **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree.
56591
57027
  - **Exception \u2014 Project memory:** You MAY read and write to files under \`.fusion/memory/\` at the project root to save durable project learnings.
56592
57028
  - **Exception \u2014 Task attachments:** You MAY read files under \`.fusion/tasks/{taskId}/attachments/\` at the project root for context.
57029
+ - **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
57030
  - **Shell commands** run inside the worktree by default. Avoid using \`cd\` to navigate outside the worktree.
56594
57031
 
56595
57032
  ## Begin
56596
57033
 
56597
57034
  ${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.
57035
+ Use \`fn_task_update\` to report progress on every step transition.
57036
+ Use \`fn_task_log\` for important actions and decisions.
57037
+ Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
56601
57038
  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()\`
57039
+ When all steps are complete: call \`fn_task_done()\`
56603
57040
 
56604
- If a build command is configured, run that exact command in this worktree before calling \`task_done()\`.
57041
+ If a build command is configured, run that exact command in this worktree before calling \`fn_task_done()\`.
56605
57042
  Treat a non-zero exit code as a blocking failure. Do not claim success without a real passing run.
56606
57043
  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.
57044
+ 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.
57045
+ If the repo has a typecheck command, run it before \`fn_task_done()\` and fix any failures it reports.
57046
+ Use \`fn_task_create\` for truly separate follow-up work, not for fixes required to get tests, build, or typecheck back to green.
56610
57047
  If lint is configured and failing, fix that too before completion.
56611
57048
  **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
57049
  }
@@ -56721,22 +57158,22 @@ You are working in a git worktree isolated from the main branch. Your job is to
56721
57158
  You have tools to report progress. The board updates in real-time.
56722
57159
 
56723
57160
  **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")\`
57161
+ - Before starting a step: \`fn_task_update(step=N, status="in-progress")\`
57162
+ - After completing a step: \`fn_task_update(step=N, status="done")\`
57163
+ - If skipping a step: \`fn_task_update(step=N, status="skipped")\`
56727
57164
 
56728
- **Logging important actions:** \`task_log(message="what happened")\`
57165
+ **Logging important actions:** \`fn_task_log(message="what happened")\`
56729
57166
 
56730
- **Out-of-scope work found during execution:** \`task_create(description="what needs doing")\`
57167
+ **Out-of-scope work found during execution:** \`fn_task_create(description="what needs doing")\`
56731
57168
  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"])\`
57169
+ \`fn_task_create(description="load door sounds", dependencies=[])\` \u2192 returns KB-050
57170
+ \`fn_task_create(description="play sound on door open/close", dependencies=["KB-050"])\`
56734
57171
 
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.
57172
+ **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
57173
 
56737
- ## Cross-model review via review_step tool
57174
+ ## Cross-model review via fn_review_step tool
56738
57175
 
56739
- You have a \`review_step\` tool. It spawns a SEPARATE reviewer agent (different
57176
+ You have a \`fn_review_step\` tool. It spawns a SEPARATE reviewer agent (different
56740
57177
  model, read-only access) to independently assess your work.
56741
57178
 
56742
57179
  **When to call it** \u2014 based on the Review Level in the PROMPT.md:
@@ -56744,8 +57181,8 @@ model, read-only access) to independently assess your work.
56744
57181
  | Review Level | Before implementing | After implementing + committing |
56745
57182
  |-------------|--------------------|---------------------------------|
56746
57183
  | 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)\` |
57184
+ | 1 (Plan) | \`fn_review_step(step, "plan", step_name)\` | \u2014 |
57185
+ | 2 (Plan+Code) | \`fn_review_step(step, "plan", step_name)\` | \`fn_review_step(step, "code", step_name, baseline)\` |
56749
57186
  | 3 (Full) | plan review | code review + test review |
56750
57187
 
56751
57188
  **Skip reviews for** Step 0 (Preflight) and the final documentation/delivery step.
@@ -56754,13 +57191,13 @@ model, read-only access) to independently assess your work.
56754
57191
  1. Before starting a step, capture baseline: \`git rev-parse HEAD\`
56755
57192
  2. Implement the step
56756
57193
  3. Commit
56757
- 4. Call \`review_step\` with the baseline SHA so the reviewer sees only your changes
57194
+ 4. Call \`fn_review_step\` with the baseline SHA so the reviewer sees only your changes
56758
57195
 
56759
57196
  **Handling verdicts:**
56760
57197
  - **APPROVE** \u2192 proceed to next step
56761
57198
  - **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.
57199
+ and re-run \`fn_review_step(type="code")\` before the step can be marked done.
57200
+ \`fn_task_update(status="done")\` will be rejected until the code review passes.
56764
57201
  - **REVISE (plan review)** \u2192 advisory. Incorporate the feedback at your discretion
56765
57202
  and proceed with implementation. No re-review is required.
56766
57203
  - **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 +57207,13 @@ model, read-only access) to independently assess your work.
56770
57207
 
56771
57208
  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
57209
 
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)
57210
+ - **Save a document:** \`fn_task_document_write(key="plan", content="...")\`
57211
+ - **Read a document:** \`fn_task_document_read(key="plan")\`
57212
+ - **List all documents:** \`fn_task_document_read()\` (no key)
56776
57213
 
56777
57214
  Documents are versioned \u2014 each write creates a new revision. Use meaningful keys like "plan", "notes", "research", "architecture".
56778
57215
 
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.
57216
+ **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
57217
 
56781
57218
  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
57219
 
@@ -56792,6 +57229,7 @@ You are running in an **isolated git worktree**. This means:
56792
57229
  - **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
57230
  - **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
57231
  - **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.
57232
+ - **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
57233
  - **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree.
56796
57234
 
56797
57235
  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 +57240,7 @@ If you attempt to write to a path outside the worktree, the file tools will reje
56802
57240
  - Read "Context to Read First" files before starting
56803
57241
  - Follow the "Do NOT" section strictly
56804
57242
  - 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
57243
+ - Use \`fn_task_create\` for genuinely separate follow-up work, not for mandatory fixes required to make this task land cleanly
56806
57244
  - Update documentation listed in "Must Update" and check "Check If Affected"
56807
57245
  - NEVER delete, remove, or gut modules, interfaces, settings, exports, or test files outside your File Scope
56808
57246
  - NEVER remove features as "cleanup" \u2014 if something seems unused, create a task for investigation instead
@@ -56813,14 +57251,14 @@ If you attempt to write to a path outside the worktree, the file tools will reje
56813
57251
 
56814
57252
  You can spawn child agents to handle parallel work or specialized sub-tasks:
56815
57253
 
56816
- **When to use \`spawn_agent\`:**
57254
+ **When to use \`fn_spawn_agent\`:**
56817
57255
  - Parallel work that can be divided into independent chunks
56818
57256
  - Specialized tasks requiring different expertise or tools
56819
57257
  - Delegation of sub-tasks to specialized agents
56820
57258
 
56821
57259
  **How to spawn:**
56822
57260
  \`\`\`javascript
56823
- spawn_agent({
57261
+ fn_spawn_agent({
56824
57262
  name: "researcher",
56825
57263
  role: "engineer",
56826
57264
  task: "Research best practices for authentication in React applications"
@@ -56830,7 +57268,7 @@ spawn_agent({
56830
57268
  **Child agent behavior:**
56831
57269
  - Each child runs in its own git worktree (branched from your worktree)
56832
57270
  - Children execute autonomously and report completion
56833
- - When you end (task_done), all spawned children are terminated
57271
+ - When you end (fn_task_done), all spawned children are terminated
56834
57272
  - Check AgentStore for spawned agent status
56835
57273
 
56836
57274
  **Limits:**
@@ -56840,13 +57278,13 @@ spawn_agent({
56840
57278
  ## Completion
56841
57279
  After all steps are done, lint passes, tests pass, typecheck passes, and docs are updated:
56842
57280
  \`\`\`bash
56843
- Call \`task_done()\` to signal completion.
57281
+ Call \`fn_task_done()\` to signal completion.
56844
57282
  \`\`\`
56845
57283
 
56846
57284
  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()\`
57285
+ - Run the exact build command in the current worktree before \`fn_task_done()\`
56848
57286
  - 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
57287
+ - If the build fails, do NOT call \`fn_task_done()\`; keep working until it passes
56850
57288
 
56851
57289
  Lint, tests, and typecheck are also hard quality gates:
56852
57290
  - Keep fixing failures until lint, the configured/full test suite, and typecheck all pass
@@ -57104,15 +57542,15 @@ Lint, tests, and typecheck are also hard quality gates:
57104
57542
  }
57105
57543
  /**
57106
57544
  * 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).
57545
+ * Used to detect tasks that called fn_task_done() but never transitioned to in-review
57546
+ * (e.g., killed by stuck detector after fn_task_done but before moveTask).
57109
57547
  */
57110
57548
  isTaskWorkComplete(task) {
57111
57549
  if (task.steps.length === 0) return false;
57112
57550
  return task.steps.every((s) => s.status === "done" || s.status === "skipped");
57113
57551
  }
57114
57552
  isNoProgressNoTaskDoneFailure(task) {
57115
- return task.status === "failed" && task.error?.includes("without calling task_done") === true && task.steps.every((step) => step.status === "pending");
57553
+ return task.status === "failed" && task.error?.includes("without calling fn_task_done") === true && task.steps.every((step) => step.status === "pending");
57116
57554
  }
57117
57555
  async clearResumeFailureState(task) {
57118
57556
  const updates = {};
@@ -57288,7 +57726,7 @@ Lint, tests, and typecheck are also hard quality gates:
57288
57726
  continue;
57289
57727
  }
57290
57728
  if (this.isNoProgressNoTaskDoneFailure(task)) {
57291
- executorLog.log(`${task.id} failed without task_done and has no step progress \u2014 leaving for self-healing requeue`);
57729
+ executorLog.log(`${task.id} failed without fn_task_done and has no step progress \u2014 leaving for self-healing requeue`);
57292
57730
  continue;
57293
57731
  }
57294
57732
  executorLog.log(`Resuming ${task.id}: ${task.title || task.description.slice(0, 60)}`);
@@ -57513,13 +57951,15 @@ Lint, tests, and typecheck are also hard quality gates:
57513
57951
  await this.store.logEntry(task.id, `Worktree created at ${worktreePath}`, void 0, this.currentRunContext);
57514
57952
  }
57515
57953
  if (settings.worktreeInitCommand) {
57954
+ const initStartedAt = Date.now();
57516
57955
  try {
57517
57956
  const initResult = await runConfiguredCommand(settings.worktreeInitCommand, worktreePath, 3e5);
57518
57957
  if (initResult.spawnError || initResult.timedOut || initResult.exitCode !== 0) {
57519
57958
  throw new Error(configuredCommandErrorMessage(initResult));
57520
57959
  }
57521
- await this.store.logEntry(task.id, "Worktree init command completed", settings.worktreeInitCommand, this.currentRunContext);
57960
+ await this.store.logEntry(task.id, `[timing] Worktree init command completed in ${Date.now() - initStartedAt}ms`, settings.worktreeInitCommand, this.currentRunContext);
57522
57961
  } catch (err) {
57962
+ await this.store.logEntry(task.id, `[timing] Worktree init command failed after ${Date.now() - initStartedAt}ms`, void 0, this.currentRunContext);
57523
57963
  const execError = err instanceof Error ? err : new Error(String(err));
57524
57964
  const message = "stderr" in execError && typeof execError.stderr === "string" ? String(execError.stderr) : execError.message;
57525
57965
  executorLog.error(`${task.id}: worktree init command failed \u2014 first test run will likely fail: ${message}`);
@@ -57534,12 +57974,13 @@ Lint, tests, and typecheck are also hard quality gates:
57534
57974
  if (settings.setupScript) {
57535
57975
  const scriptCommand = settings.scripts?.[settings.setupScript];
57536
57976
  if (scriptCommand) {
57977
+ const setupStartedAt = Date.now();
57537
57978
  try {
57538
57979
  const setupResult = await runConfiguredCommand(scriptCommand, worktreePath, 12e4);
57539
57980
  if (setupResult.spawnError || setupResult.timedOut || setupResult.exitCode !== 0) {
57540
57981
  throw new Error(configuredCommandErrorMessage(setupResult));
57541
57982
  }
57542
- await this.store.logEntry(task.id, `Setup script '${settings.setupScript}' completed`, scriptCommand, this.currentRunContext);
57983
+ await this.store.logEntry(task.id, `[timing] Setup script '${settings.setupScript}' completed in ${Date.now() - setupStartedAt}ms`, scriptCommand, this.currentRunContext);
57543
57984
  } catch (err) {
57544
57985
  const execError = err instanceof Error ? err : new Error(String(err));
57545
57986
  const message = "stderr" in execError && typeof execError.stderr === "string" ? String(execError.stderr) : execError.message;
@@ -57705,7 +58146,7 @@ Lint, tests, and typecheck are also hard quality gates:
57705
58146
  await retryableStepWork();
57706
58147
  }
57707
58148
  } catch (err) {
57708
- const errorMessage = err instanceof Error ? err.message : String(err);
58149
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
57709
58150
  if (this.depAborted.has(task.id)) {
57710
58151
  this.depAborted.delete(task.id);
57711
58152
  await this.handleDepAbortCleanup(task.id, worktreePath);
@@ -57749,7 +58190,10 @@ Lint, tests, and typecheck are also hard quality gates:
57749
58190
  stuckRequeue = null;
57750
58191
  return;
57751
58192
  }
57752
- executorLog.error(`\u2717 ${task.id} transient error retries exhausted: ${errorMessage}`);
58193
+ executorLog.error(`\u2717 ${task.id} transient error retries exhausted: ${errorDetail}`);
58194
+ if (errorStack) {
58195
+ await this.store.logEntry(task.id, `Transient error retries exhausted: ${errorMessage}`, errorStack, this.currentRunContext);
58196
+ }
57753
58197
  await this.store.updateTask(task.id, {
57754
58198
  status: "failed",
57755
58199
  error: errorMessage,
@@ -57760,8 +58204,8 @@ Lint, tests, and typecheck are also hard quality gates:
57760
58204
  executorLog.log(`\u2717 ${task.id} transient retries exhausted \u2192 in-review`);
57761
58205
  this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
57762
58206
  } 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);
58207
+ executorLog.error(`\u2717 ${task.id} step-session execution failed:`, errorDetail);
58208
+ await this.store.logEntry(task.id, `Step-session execution failed: ${errorMessage}`, errorStack ?? errorDetail, this.currentRunContext);
57765
58209
  await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
57766
58210
  await this.store.moveTask(task.id, "in-review");
57767
58211
  executorLog.log(`\u2717 ${task.id} step-session execution failed \u2192 in-review`);
@@ -57811,7 +58255,7 @@ Lint, tests, and typecheck are also hard quality gates:
57811
58255
  const reflectionTools = this.options.reflectionService && settings.reflectionEnabled && assignedAgentId ? [createReflectOnPerformanceTool(this.options.reflectionService, assignedAgentId)] : [];
57812
58256
  const assignedAgent = assignedAgentId && this.options.agentStore ? await this.options.agentStore.getAgent(assignedAgentId).catch(() => null) : null;
57813
58257
  if (executionMode === "fast") {
57814
- executorLog.log(`${task.id}: fast mode \u2014 review_step tool not injected`);
58258
+ executorLog.log(`${task.id}: fast mode \u2014 fn_review_step tool not injected`);
57815
58259
  }
57816
58260
  const customTools = [
57817
58261
  this.createTaskUpdateTool(task.id, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector),
@@ -57821,7 +58265,7 @@ Lint, tests, and typecheck are also hard quality gates:
57821
58265
  this.createTaskDoneTool(task.id, () => {
57822
58266
  taskDone = true;
57823
58267
  }),
57824
- // Skip review_step tool in fast mode — fast mode bypasses automated review gates
58268
+ // Skip fn_review_step tool in fast mode — fast mode bypasses automated review gates
57825
58269
  ...executionMode !== "fast" ? [
57826
58270
  this.createReviewStepTool(task.id, worktreePath, detail.prompt, codeReviewVerdicts, sessionRef, stepCheckpoints, detail, stuckDetector)
57827
58271
  ] : [],
@@ -57978,7 +58422,7 @@ Lint, tests, and typecheck are also hard quality gates:
57978
58422
  "3. Review the PROMPT.md steps to see which are still pending",
57979
58423
  "",
57980
58424
  "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.",
58425
+ "If the current step is complete, call fn_task_update to mark it done and move to the next step.",
57982
58426
  "If you're stuck on a problem, try a simpler or alternative solution.",
57983
58427
  "",
57984
58428
  "Continue the task from where you left off."
@@ -58016,8 +58460,8 @@ Lint, tests, and typecheck are also hard quality gates:
58016
58460
  const implicitCheck = await this.store.getTask(task.id);
58017
58461
  if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
58018
58462
  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);
58463
+ executorLog.log(`${task.id} all steps done \u2014 treating as implicit fn_task_done`);
58464
+ 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
58465
  }
58022
58466
  }
58023
58467
  if (taskDone) {
@@ -58054,11 +58498,11 @@ Lint, tests, and typecheck are also hard quality gates:
58054
58498
  while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
58055
58499
  taskDoneSessionRetries++;
58056
58500
  executorLog.log(
58057
- `\u26A0 ${task.id} finished without task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
58501
+ `\u26A0 ${task.id} finished without fn_task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
58058
58502
  );
58059
58503
  await this.store.logEntry(
58060
58504
  task.id,
58061
- `Agent finished without calling task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
58505
+ `Agent finished without calling fn_task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
58062
58506
  void 0,
58063
58507
  this.currentRunContext
58064
58508
  );
@@ -58100,10 +58544,10 @@ Lint, tests, and typecheck are also hard quality gates:
58100
58544
  });
58101
58545
  stuckDetector?.trackTask(task.id, retrySession);
58102
58546
  const retryPrompt = [
58103
- "Your previous session ended without calling the task_done tool.",
58547
+ "Your previous session ended without calling the fn_task_done tool.",
58104
58548
  "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.",
58549
+ "1. If the work is done, call fn_task_done with a summary of what was accomplished.",
58550
+ "2. If there is remaining work, finish it and then call fn_task_done.",
58107
58551
  "",
58108
58552
  "Original task:",
58109
58553
  buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
@@ -58115,8 +58559,8 @@ Lint, tests, and typecheck are also hard quality gates:
58115
58559
  const implicitCheck = await this.store.getTask(task.id);
58116
58560
  if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
58117
58561
  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);
58562
+ executorLog.log(`${task.id} all steps done \u2014 treating as implicit fn_task_done`);
58563
+ 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
58564
  }
58121
58565
  }
58122
58566
  }
@@ -58148,7 +58592,7 @@ Lint, tests, and typecheck are also hard quality gates:
58148
58592
  } else {
58149
58593
  const priorRequeues = task.taskDoneRetryCount ?? 0;
58150
58594
  const nextRequeueCount = priorRequeues + 1;
58151
- const errorMessage = `Agent finished without calling task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
58595
+ const errorMessage = `Agent finished without calling fn_task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
58152
58596
  if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
58153
58597
  await this.store.updateTask(task.id, {
58154
58598
  status: "failed",
@@ -58167,7 +58611,7 @@ Lint, tests, and typecheck are also hard quality gates:
58167
58611
  await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
58168
58612
  await this.store.logEntry(task.id, `${errorMessage} \u2014 moved to in-review for inspection`, void 0, this.currentRunContext);
58169
58613
  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`);
58614
+ executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 no fn_task_done \u2192 in-review`);
58171
58615
  }
58172
58616
  this.options.onError?.(task, new Error(errorMessage));
58173
58617
  }
@@ -58203,7 +58647,7 @@ Lint, tests, and typecheck are also hard quality gates:
58203
58647
  await retryableWork();
58204
58648
  }
58205
58649
  } catch (err) {
58206
- const errorMessage = err instanceof Error ? err.message : String(err);
58650
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
58207
58651
  if (this.depAborted.has(task.id)) {
58208
58652
  this.depAborted.delete(task.id);
58209
58653
  await this.handleDepAbortCleanup(task.id, worktreePath);
@@ -58271,7 +58715,7 @@ Lint, tests, and typecheck are also hard quality gates:
58271
58715
  "2. Identify the most critical remaining work",
58272
58716
  "3. Complete it with a simpler, more focused approach",
58273
58717
  "",
58274
- "Do not repeat what's already been done. Just complete the task and call task_done."
58718
+ "Do not repeat what's already been done. Just complete the task and call fn_task_done."
58275
58719
  ].join("\n");
58276
58720
  await promptWithFallback(activeEntry.session, reducedPrompt);
58277
58721
  checkSessionError(activeEntry.session);
@@ -58346,8 +58790,8 @@ Lint, tests, and typecheck are also hard quality gates:
58346
58790
  await this.store.moveTask(task.id, "todo");
58347
58791
  return;
58348
58792
  }
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);
58793
+ executorLog.error(`\u2717 ${task.id} transient error retries exhausted (${MAX_RECOVERY_RETRIES} attempts): ${errorDetail}`);
58794
+ await this.store.logEntry(task.id, `Transient error retries exhausted after ${MAX_RECOVERY_RETRIES} attempts: ${errorMessage}`, errorStack ?? errorDetail, this.currentRunContext);
58351
58795
  await this.store.updateTask(task.id, {
58352
58796
  status: "failed",
58353
58797
  error: errorMessage,
@@ -58359,8 +58803,8 @@ Lint, tests, and typecheck are also hard quality gates:
58359
58803
  this.options.onError?.(task, err instanceof Error ? err : new Error(errorMessage));
58360
58804
  return;
58361
58805
  }
58362
- executorLog.error(`\u2717 ${task.id} execution failed:`, errorMessage);
58363
- await this.store.logEntry(task.id, `Execution failed: ${errorMessage}`, void 0, this.currentRunContext);
58806
+ executorLog.error(`\u2717 ${task.id} execution failed:`, errorDetail);
58807
+ await this.store.logEntry(task.id, `Execution failed: ${errorMessage}`, errorStack ?? errorDetail, this.currentRunContext);
58364
58808
  await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
58365
58809
  await this.store.moveTask(task.id, "in-review");
58366
58810
  executorLog.log(`\u2717 ${task.id} execution failed \u2192 in-review`);
@@ -58408,7 +58852,7 @@ Lint, tests, and typecheck are also hard quality gates:
58408
58852
  createTaskUpdateTool(taskId, codeReviewVerdicts, sessionRef, stepCheckpoints, stuckDetector) {
58409
58853
  const store = this.store;
58410
58854
  return {
58411
- name: "task_update",
58855
+ name: "fn_task_update",
58412
58856
  label: "Update Step",
58413
58857
  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
58858
  parameters: taskUpdateParams,
@@ -58421,7 +58865,7 @@ Lint, tests, and typecheck are also hard quality gates:
58421
58865
  return {
58422
58866
  content: [{
58423
58867
  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.`
58868
+ 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
58869
  }],
58426
58870
  details: {}
58427
58871
  };
@@ -58460,7 +58904,7 @@ Lint, tests, and typecheck are also hard quality gates:
58460
58904
  createTaskAddDepTool(taskId) {
58461
58905
  const store = this.store;
58462
58906
  return {
58463
- name: "task_add_dep",
58907
+ name: "fn_task_add_dep",
58464
58908
  label: "Add Dependency",
58465
58909
  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
58910
  parameters: taskAddDepParams,
@@ -58530,7 +58974,7 @@ Lint, tests, and typecheck are also hard quality gates:
58530
58974
  createTaskDoneTool(taskId, onDone) {
58531
58975
  const store = this.store;
58532
58976
  return {
58533
- name: "task_done",
58977
+ name: "fn_task_done",
58534
58978
  label: "Mark Task Done",
58535
58979
  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
58980
  parameters: Type4.Object({
@@ -58545,7 +58989,7 @@ Lint, tests, and typecheck are also hard quality gates:
58545
58989
  return {
58546
58990
  content: [{
58547
58991
  type: "text",
58548
- text: `Cannot mark task done yet \u2014 ${completionBlocker}. Resolve the blocker before calling task_done().`
58992
+ text: `Cannot mark task done yet \u2014 ${completionBlocker}. Resolve the blocker before calling fn_task_done().`
58549
58993
  }],
58550
58994
  details: {}
58551
58995
  };
@@ -58569,7 +59013,7 @@ Lint, tests, and typecheck are also hard quality gates:
58569
59013
  };
58570
59014
  }
58571
59015
  /**
58572
- * Create the review_step tool for the executor agent.
59016
+ * Create the fn_review_step tool for the executor agent.
58573
59017
  *
58574
59018
  * When the reviewer returns a RETHINK verdict, this tool:
58575
59019
  * 1. Runs `git reset --hard <baseline>` to revert file changes
@@ -58581,7 +59025,7 @@ Lint, tests, and typecheck are also hard quality gates:
58581
59025
  const store = this.store;
58582
59026
  const options = this.options;
58583
59027
  return {
58584
- name: "review_step",
59028
+ name: "fn_review_step",
58585
59029
  label: "Review Step",
58586
59030
  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
59031
  parameters: reviewStepParams,
@@ -58651,7 +59095,7 @@ Lint, tests, and typecheck are also hard quality gates:
58651
59095
  if (reviewType === "code") {
58652
59096
  text = `REVISE \u2014 this step cannot be marked done until the code review passes.
58653
59097
 
58654
- Fix the issues below, commit your changes, and call review_step(step=${step}, type="code", step_name="${step_name}", baseline="<new SHA>") again.
59098
+ 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
59099
 
58656
59100
  ${result.review}`;
58657
59101
  } else {
@@ -58851,7 +59295,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
58851
59295
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
58852
59296
  return;
58853
59297
  }
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.";
59298
+ 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
59299
  const revisionSectionHeader = "## Workflow Revision Instructions";
58856
59300
  const revisionSectionContent = `${revisionSectionHeader}
58857
59301
 
@@ -59150,6 +59594,7 @@ ${failureFeedback}
59150
59594
  await this.store.logEntry(task.id, `[pre-merge] Starting workflow step: ${ws.name} (${stepMode} mode)`);
59151
59595
  executorLog.log(`${task.id} \u2014 [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
59152
59596
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
59597
+ const stepStartedAtMs = Date.now();
59153
59598
  results.push({
59154
59599
  workflowStepId: ws.id,
59155
59600
  workflowStepName: ws.name,
@@ -59162,6 +59607,7 @@ ${failureFeedback}
59162
59607
  const result = stepMode === "script" ? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings) : await this.executeWorkflowStep(task, ws, worktreePath, settings);
59163
59608
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
59164
59609
  if (result.success) {
59610
+ await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
59165
59611
  await this.store.logEntry(task.id, `[pre-merge] Workflow step completed: ${ws.name}`);
59166
59612
  executorLog.log(`${task.id} \u2014 [pre-merge] workflow step passed: ${ws.name}`);
59167
59613
  const existingIdx = results.findIndex((r) => r.workflowStepId === ws.id);
@@ -59175,6 +59621,7 @@ ${failureFeedback}
59175
59621
  }
59176
59622
  await this.store.updateTask(task.id, { workflowStepResults: results });
59177
59623
  } else if (result.revisionRequested) {
59624
+ await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' requested revision after ${Date.now() - stepStartedAtMs}ms`);
59178
59625
  await this.store.logEntry(
59179
59626
  task.id,
59180
59627
  `[pre-merge] Workflow step requested revision: ${ws.name}`,
@@ -59198,6 +59645,7 @@ ${failureFeedback}
59198
59645
  stepName: ws.name
59199
59646
  };
59200
59647
  } else {
59648
+ await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' failed after ${Date.now() - stepStartedAtMs}ms`);
59201
59649
  await this.store.logEntry(
59202
59650
  task.id,
59203
59651
  `[pre-merge] Workflow step failed: ${ws.name}`,
@@ -59222,14 +59670,14 @@ ${failureFeedback}
59222
59670
  };
59223
59671
  }
59224
59672
  } catch (err) {
59225
- const errorMessage = err instanceof Error ? err.message : String(err);
59673
+ const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
59226
59674
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
59227
59675
  await this.store.logEntry(
59228
59676
  task.id,
59229
59677
  `[pre-merge] Workflow step failed: ${ws.name}`,
59230
- errorMessage
59678
+ errorStack ?? errorDetail
59231
59679
  );
59232
- executorLog.error(`${task.id} \u2014 [pre-merge] workflow step error: ${ws.name} \u2014 ${errorMessage}`);
59680
+ executorLog.error(`${task.id} \u2014 [pre-merge] workflow step error: ${ws.name} \u2014 ${errorDetail}`);
59233
59681
  const existingIdx = results.findIndex((r) => r.workflowStepId === ws.id);
59234
59682
  if (existingIdx >= 0) {
59235
59683
  results[existingIdx] = {
@@ -59855,7 +60303,7 @@ Review the work done in this worktree and evaluate it against the criteria in yo
59855
60303
  /**
59856
60304
  * When the engine restarts mid-step, an `in-progress` step may have already
59857
60305
  * 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
60306
+ * been flipped to `done` by the agent's next `fn_task_update` call. Without
59859
60307
  * intervention, the next executor pass re-enters the step and replays plan
59860
60308
  * + code review, which we've measured at 5–20 min of pure waste per restart.
59861
60309
  *
@@ -60118,14 +60566,14 @@ Review the work done in this worktree and evaluate it against the criteria in yo
60118
60566
  }
60119
60567
  }
60120
60568
  /**
60121
- * Create the spawn_agent tool definition.
60569
+ * Create the fn_spawn_agent tool definition.
60122
60570
  * Allows the parent agent to spawn child agents with delegated tasks.
60123
60571
  */
60124
60572
  createSpawnAgentTool(taskId, worktreePath, settings) {
60125
60573
  return {
60126
- name: "spawn_agent",
60574
+ name: "fn_spawn_agent",
60127
60575
  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.",
60576
+ 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
60577
  parameters: spawnAgentParams,
60130
60578
  execute: async (_id, params) => {
60131
60579
  const { name, role, task: taskPrompt } = params;
@@ -60645,6 +61093,7 @@ var init_scheduler = __esm({
60645
61093
  }
60646
61094
  }
60647
61095
  if (todo.length === 0) return;
61096
+ todo = sortTasksByPriorityThenAgeAndId(todo);
60648
61097
  const activeScopes = /* @__PURE__ */ new Map();
60649
61098
  if (settings.groupOverlappingFiles) {
60650
61099
  for (const t of inProgress) {
@@ -64822,14 +65271,14 @@ var init_agent_heartbeat = __esm({
64822
65271
  Your job:
64823
65272
  1. Check your assigned task \u2014 read the description and PROMPT.md if present.
64824
65273
  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.
65274
+ 3. Use fn_task_create to spawn follow-up work, fn_task_log to record observations.
65275
+ 4. Use fn_task_document_write to save durable findings, plans, or research notes.
65276
+ 5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
64828
65277
 
64829
65278
  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.
65279
+ You have readonly file access plus fn_task_create, fn_task_log, and fn_task_document tools.
64831
65280
 
64832
- **Task Documents:** Save important findings with task_document_write(key="...", content="...").
65281
+ **Task Documents:** Save important findings with fn_task_document_write(key="...", content="...").
64833
65282
  Documents persist across sessions and are visible in the dashboard's Documents tab.
64834
65283
 
64835
65284
  ## Memory Boundaries
@@ -64842,12 +65291,12 @@ You may receive an Agent Memory section and a Project Memory section.
64842
65291
  ## Processing Messages
64843
65292
 
64844
65293
  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.
65294
+ 1. Use fn_read_messages to check your inbox for unread messages.
64846
65295
  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.
65296
+ - If the message requires a response, use fn_send_message to reply.
65297
+ - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output.
65298
+ - If the message is informational, acknowledge it by logging with fn_task_log.
65299
+ - If the message requests work, create a follow-up task with fn_task_create or handle it directly.
64851
65300
  3. After processing messages, continue with your normal heartbeat duties.
64852
65301
 
64853
65302
  When sending messages:
@@ -64860,17 +65309,17 @@ When sending messages:
64860
65309
  Your job:
64861
65310
  1. Review your context \u2014 check messages, memory, and project state.
64862
65311
  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.
65312
+ 3. Use fn_task_create to spawn follow-up work.
65313
+ 4. Use fn_list_agents and fn_delegate_task to coordinate with other agents.
65314
+ 5. Call fn_heartbeat_done when finished with an optional summary of what was accomplished.
64866
65315
 
64867
65316
  Keep work lightweight \u2014 this is a single-pass ambient check, not a full implementation run.
64868
65317
  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)
65318
+ - fn_task_create
65319
+ - fn_list_agents and fn_delegate_task
65320
+ - fn_memory_search, fn_memory_get, and fn_memory_append
65321
+ - fn_heartbeat_done
65322
+ - fn_send_message and fn_read_messages when messaging is enabled for this run (they may not always be available)
64874
65323
 
64875
65324
  ## Memory Boundaries
64876
65325
 
@@ -64882,12 +65331,12 @@ You may receive an Agent Memory section and a Project Memory section.
64882
65331
  ## Processing Messages
64883
65332
 
64884
65333
  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.
65334
+ 1. If fn_read_messages is available, use it to check your inbox for unread messages.
64886
65335
  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.
65336
+ - If the message requires a response and fn_send_message is available, use fn_send_message to reply.
65337
+ - When replying, include 'reply_to_message_id' with the original message ID from fn_read_messages output.
65338
+ - If the message is informational, acknowledge it and respond via fn_send_message when appropriate.
65339
+ - If the message requests work, create a follow-up task with fn_task_create.
64891
65340
  3. After processing messages, continue with your ambient work.
64892
65341
 
64893
65342
  When sending messages:
@@ -65294,7 +65743,7 @@ When sending messages:
65294
65743
  * Implements the Paperclip-style execution model:
65295
65744
  * 1. Wake — start a heartbeat run record
65296
65745
  * 2. Check inbox — resolve the agent's assigned task
65297
- * 3. Work — run a lightweight agent session with readonly tools + task_create/task_log
65746
+ * 3. Work — run a lightweight agent session with readonly tools + fn_task_create/fn_task_log
65298
65747
  * 4. Exit — record results and complete the run
65299
65748
  *
65300
65749
  * Budget governance:
@@ -65544,7 +65993,7 @@ When sending messages:
65544
65993
  stdoutExcerpt += delta.slice(0, remaining);
65545
65994
  };
65546
65995
  const heartbeatDoneTool = {
65547
- name: "heartbeat_done",
65996
+ name: "fn_heartbeat_done",
65548
65997
  label: "Heartbeat Done",
65549
65998
  description: "Signal that the heartbeat execution is complete. Call when finished.",
65550
65999
  parameters: heartbeatDoneParams,
@@ -65673,16 +66122,16 @@ When sending messages:
65673
66122
  "You have identity (soul, instructions, and/or memory) loaded, which means you can perform",
65674
66123
  "useful ambient work. Here are some things you can do:",
65675
66124
  "",
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.",
66125
+ "1. **Check your messages** \u2014 Use fn_read_messages to review any pending messages",
66126
+ " and use fn_send_message with reply_to_message_id when responding.",
65678
66127
  "",
65679
- "2. **Create new tasks** \u2014 Use task_create to spawn follow-up work that needs",
66128
+ "2. **Create new tasks** \u2014 Use fn_task_create to spawn follow-up work that needs",
65680
66129
  " to be done. This is useful for surfacing issues or ideas you discover.",
65681
66130
  "",
65682
- "3. **Delegate work** \u2014 Use list_agents to discover available agents and",
65683
- " delegate_task to assign work to them.",
66131
+ "3. **Delegate work** \u2014 Use fn_list_agents to discover available agents and",
66132
+ " fn_delegate_task to assign work to them.",
65684
66133
  "",
65685
- "4. **Update your memory** \u2014 Use memory_append to persist important learnings",
66134
+ "4. **Update your memory** \u2014 Use fn_memory_append to persist important learnings",
65686
66135
  " or context that will help you in future sessions.",
65687
66136
  "",
65688
66137
  "5. **Monitor the project** \u2014 Review the task board and identify any issues",
@@ -65691,7 +66140,7 @@ When sending messages:
65691
66140
  "",
65692
66141
  "Your soul, instructions, and memory are already loaded in the system prompt.",
65693
66142
  "Focus on work that benefits the project without requiring a specific task context.",
65694
- "Call heartbeat_done when finished."
66143
+ "Call fn_heartbeat_done when finished."
65695
66144
  ].join("\n");
65696
66145
  } else {
65697
66146
  const taskTitle = taskDetail.title ?? taskDetail.description.slice(0, 100);
@@ -65751,7 +66200,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65751
66200
  ...triggeringCommentLines,
65752
66201
  ...pendingMessagesLines,
65753
66202
  "",
65754
- "Review the task status and take appropriate action. Call heartbeat_done when finished."
66203
+ "Review the task status and take appropriate action. Call fn_heartbeat_done when finished."
65755
66204
  ].join("\n");
65756
66205
  }
65757
66206
  await promptWithFallback2(session, executionPrompt);
@@ -65783,12 +66232,12 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65783
66232
  });
65784
66233
  heartbeatLog.log(`Heartbeat completed for ${agentId} (${toolCallCount} tool calls, ~${estimatedOutputTokens} output tokens)`);
65785
66234
  } catch (err) {
65786
- const errorMessage = err instanceof Error ? err.message : String(err);
65787
- heartbeatLog.error(`Heartbeat execution failed for ${agentId}: ${errorMessage}`);
66235
+ const errorDetail = formatError(err).detail;
66236
+ heartbeatLog.error(`Heartbeat execution failed for ${agentId}: ${errorDetail}`);
65788
66237
  await flushAgentLogger();
65789
66238
  await this.completeRun(agentId, run.id, {
65790
66239
  status: "failed",
65791
- stderrExcerpt: errorMessage,
66240
+ stderrExcerpt: errorDetail,
65792
66241
  stdoutExcerpt: stdoutExcerpt || void 0
65793
66242
  });
65794
66243
  } finally {
@@ -65807,13 +66256,14 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65807
66256
  }
65808
66257
  return await this.store.getRunDetail(agentId, run.id);
65809
66258
  } catch (err) {
66259
+ const errorDetail = formatError(err).detail;
65810
66260
  const errorMessage = err instanceof Error ? err.message : String(err);
65811
- heartbeatLog.error(`Heartbeat execution error for ${agentId}: ${errorMessage}`);
66261
+ heartbeatLog.error(`Heartbeat execution error for ${agentId}: ${errorDetail}`);
65812
66262
  await flushAgentLogger();
65813
66263
  try {
65814
66264
  await this.completeRun(agentId, run.id, {
65815
66265
  status: "failed",
65816
- stderrExcerpt: errorMessage
66266
+ stderrExcerpt: errorDetail
65817
66267
  });
65818
66268
  } catch (completeRunErr) {
65819
66269
  const completeRunErrMsg = completeRunErr instanceof Error ? completeRunErr.message : String(completeRunErr);
@@ -65851,7 +66301,7 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65851
66301
  *
65852
66302
  * @param agentId - The agent ID (used for tracking and logging)
65853
66303
  * @param taskStore - TaskStore for task creation and logging
65854
- * @param taskId - The assigned task ID (for task_log context)
66304
+ * @param taskId - The assigned task ID (for fn_task_log context)
65855
66305
  * @param runContext - Optional run context for mutation correlation
65856
66306
  * @param audit - Optional run auditor for audit trail (FN-1404)
65857
66307
  * @param messageStore - Optional MessageStore for messaging tools
@@ -69927,7 +70377,7 @@ var init_sse_buffer = __esm({
69927
70377
  });
69928
70378
 
69929
70379
  // ../dashboard/src/ai-session-diagnostics.ts
69930
- import { randomUUID as randomUUID10 } from "node:crypto";
70380
+ import { randomUUID as randomUUID9 } from "node:crypto";
69931
70381
  function defaultSink(level, scope, message, context) {
69932
70382
  const prefix = `[${scope}]`;
69933
70383
  const logArgs = [prefix, message, context];
@@ -69971,7 +70421,7 @@ function emit(level, scope, message, context) {
69971
70421
  const fullContext = {
69972
70422
  ...context,
69973
70423
  _emittedAt: (/* @__PURE__ */ new Date()).toISOString(),
69974
- _diagnosticsId: randomUUID10()
70424
+ _diagnosticsId: randomUUID9()
69975
70425
  };
69976
70426
  try {
69977
70427
  _sink(level, scope, message, fullContext);
@@ -70028,7 +70478,7 @@ var init_ai_session_diagnostics = __esm({
70028
70478
  });
70029
70479
 
70030
70480
  // ../dashboard/src/planning.ts
70031
- import { randomUUID as randomUUID11 } from "node:crypto";
70481
+ import { randomUUID as randomUUID10 } from "node:crypto";
70032
70482
  import { EventEmitter as EventEmitter17 } from "node:events";
70033
70483
  async function initEngine2() {
70034
70484
  try {
@@ -70222,7 +70672,7 @@ async function createSession(ip, initialPlan, _store, rootDir, promptOverrides)
70222
70672
  if (!rootDir) {
70223
70673
  throw new Error("rootDir is required for AI-powered planning sessions");
70224
70674
  }
70225
- const sessionId = randomUUID11();
70675
+ const sessionId = randomUUID10();
70226
70676
  const session = {
70227
70677
  id: sessionId,
70228
70678
  ip,
@@ -72749,7 +73199,7 @@ var init_github_poll = __esm({
72749
73199
 
72750
73200
  // ../dashboard/src/terminal.ts
72751
73201
  import { spawn as spawn2 } from "node:child_process";
72752
- import { randomUUID as randomUUID12 } from "node:crypto";
73202
+ import { randomUUID as randomUUID11 } from "node:crypto";
72753
73203
  import { EventEmitter as EventEmitter19 } from "node:events";
72754
73204
  function extractBaseCommand(command) {
72755
73205
  let trimmed = command.trim();
@@ -72909,7 +73359,7 @@ var init_terminal = __esm({
72909
73359
  if (!validation.valid) {
72910
73360
  return { sessionId: "", error: validation.error };
72911
73361
  }
72912
- const sessionId = randomUUID12();
73362
+ const sessionId = randomUUID11();
72913
73363
  const childProcess = spawn2(command, [], {
72914
73364
  cwd,
72915
73365
  shell: true,
@@ -73281,6 +73731,8 @@ function cleanupExpiredSessions3() {
73281
73731
  diagnostics4.info("Cleanup completed", {
73282
73732
  cleanedSessions,
73283
73733
  cleanedRateLimits,
73734
+ ttlMs: SESSION_TTL_MS3,
73735
+ rateLimitWindowMs: RATE_LIMIT_WINDOW_MS3,
73284
73736
  operation: "cleanup-expired"
73285
73737
  });
73286
73738
  }
@@ -73615,7 +74067,7 @@ async function initPromptOverrides() {
73615
74067
  promptOverridesReady = true;
73616
74068
  }
73617
74069
  }
73618
- var mkdtemp, access3, stat6, mkdir12, readdir8, rm, fsReadFile, fsWriteFile, upload, execFileAsync, resolveWorkflowStepRefinePrompt, promptOverridesReady, DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
74070
+ var mkdtemp, access3, stat6, mkdir12, readdir8, rm2, fsReadFile, fsWriteFile, upload, execFileAsync, resolveWorkflowStepRefinePrompt, promptOverridesReady, DEFAULT_WORKFLOW_STEP_REFINE_PROMPT;
73619
74071
  var init_routes = __esm({
73620
74072
  "../dashboard/src/routes.ts"() {
73621
74073
  "use strict";
@@ -73652,7 +74104,7 @@ var init_routes = __esm({
73652
74104
  stat: stat6,
73653
74105
  mkdir: mkdir12,
73654
74106
  readdir: readdir8,
73655
- rm,
74107
+ rm: rm2,
73656
74108
  readFile: fsReadFile,
73657
74109
  writeFile: fsWriteFile
73658
74110
  } = fsPromises);
@@ -75739,7 +76191,7 @@ var require_extension = __commonJS({
75739
76191
  if (dest[name] === void 0) dest[name] = [elem];
75740
76192
  else dest[name].push(elem);
75741
76193
  }
75742
- function parse2(header) {
76194
+ function parse(header) {
75743
76195
  const offers = /* @__PURE__ */ Object.create(null);
75744
76196
  let params = /* @__PURE__ */ Object.create(null);
75745
76197
  let mustUnescape = false;
@@ -75879,7 +76331,7 @@ var require_extension = __commonJS({
75879
76331
  }).join(", ");
75880
76332
  }).join(", ");
75881
76333
  }
75882
- module.exports = { format, parse: parse2 };
76334
+ module.exports = { format, parse };
75883
76335
  }
75884
76336
  });
75885
76337
 
@@ -75913,7 +76365,7 @@ var require_websocket = __commonJS({
75913
76365
  var {
75914
76366
  EventTarget: { addEventListener, removeEventListener }
75915
76367
  } = require_event_target();
75916
- var { format, parse: parse2 } = require_extension();
76368
+ var { format, parse } = require_extension();
75917
76369
  var { toBuffer } = require_buffer_util();
75918
76370
  var kAborted = /* @__PURE__ */ Symbol("kAborted");
75919
76371
  var protocolVersions = [8, 13];
@@ -76582,7 +77034,7 @@ var require_websocket = __commonJS({
76582
77034
  }
76583
77035
  let extensions;
76584
77036
  try {
76585
- extensions = parse2(secWebSocketExtensions);
77037
+ extensions = parse(secWebSocketExtensions);
76586
77038
  } catch (err) {
76587
77039
  const message = "Invalid Sec-WebSocket-Extensions header";
76588
77040
  abortHandshake(websocket, socket, message);
@@ -76872,7 +77324,7 @@ var require_subprotocol = __commonJS({
76872
77324
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/subprotocol.js"(exports, module) {
76873
77325
  "use strict";
76874
77326
  var { tokenChars } = require_validation();
76875
- function parse2(header) {
77327
+ function parse(header) {
76876
77328
  const protocols = /* @__PURE__ */ new Set();
76877
77329
  let start = -1;
76878
77330
  let end = -1;
@@ -76908,7 +77360,7 @@ var require_subprotocol = __commonJS({
76908
77360
  protocols.add(protocol);
76909
77361
  return protocols;
76910
77362
  }
76911
- module.exports = { parse: parse2 };
77363
+ module.exports = { parse };
76912
77364
  }
76913
77365
  });
76914
77366
 
@@ -77478,7 +77930,7 @@ var init_auth_middleware = __esm({
77478
77930
 
77479
77931
  // ../dashboard/src/server.ts
77480
77932
  import express from "express";
77481
- import { join as join34, dirname as dirname7 } from "node:path";
77933
+ import { join as join34, dirname as dirname8 } from "node:path";
77482
77934
  import { fileURLToPath as fileURLToPath2 } from "node:url";
77483
77935
  function clearAiSessionCleanupInterval() {
77484
77936
  if (!aiSessionCleanupIntervalHandle) {
@@ -77512,7 +77964,7 @@ var init_server = __esm({
77512
77964
  init_chat();
77513
77965
  init_dev_server_routes();
77514
77966
  init_auth_middleware();
77515
- __dirname = dirname7(fileURLToPath2(import.meta.url));
77967
+ __dirname = dirname8(fileURLToPath2(import.meta.url));
77516
77968
  MIN_AI_SESSION_TTL_MS = 10 * 60 * 1e3;
77517
77969
  MAX_AI_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
77518
77970
  MIN_AI_SESSION_CLEANUP_INTERVAL_MS = 60 * 1e3;
@@ -77581,7 +78033,7 @@ var init_src3 = __esm({
77581
78033
  });
77582
78034
 
77583
78035
  // src/project-context.ts
77584
- import { resolve as resolve14, dirname as dirname8 } from "node:path";
78036
+ import { resolve as resolve14, dirname as dirname9 } from "node:path";
77585
78037
  import { existsSync as existsSync28 } from "node:fs";
77586
78038
  async function resolveProject(projectNameFlag, cwd = process.cwd(), globalDir) {
77587
78039
  const central = new CentralCore(globalDir);
@@ -77672,7 +78124,7 @@ async function detectProjectFromCwd(cwd, central) {
77672
78124
  path: currentDir
77673
78125
  };
77674
78126
  }
77675
- const parentDir = dirname8(currentDir);
78127
+ const parentDir = dirname9(currentDir);
77676
78128
  if (parentDir === currentDir) {
77677
78129
  break;
77678
78130
  }
@@ -77834,11 +78286,11 @@ async function runTaskCreate(descriptionArg, attachFiles, depends, projectName)
77834
78286
  console.log(` Path: .fusion/tasks/${task.id}/`);
77835
78287
  if (attachFiles && attachFiles.length > 0) {
77836
78288
  const { readFile: readFile19 } = await import("node:fs/promises");
77837
- const { basename: basename8, extname: extname2, resolve: resolve16 } = await import("node:path");
78289
+ const { basename: basename9, extname: extname3, resolve: resolve16 } = await import("node:path");
77838
78290
  for (const filePath of attachFiles) {
77839
78291
  const resolvedPath = resolve16(filePath);
77840
- const filename = basename8(resolvedPath);
77841
- const ext = extname2(filename).toLowerCase();
78292
+ const filename = basename9(resolvedPath);
78293
+ const ext = extname3(filename).toLowerCase();
77842
78294
  const mimeType = MIME_TYPES[ext];
77843
78295
  if (!mimeType) {
77844
78296
  console.error(` \u2717 Unsupported file type: ${ext} (${filename})`);
@@ -78082,11 +78534,11 @@ async function runTaskMerge(id, projectName) {
78082
78534
  }
78083
78535
  async function runTaskAttach(id, filePath, projectName) {
78084
78536
  const { readFile: readFile19 } = await import("node:fs/promises");
78085
- const { basename: basename8, extname: extname2 } = await import("node:path");
78537
+ const { basename: basename9, extname: extname3 } = await import("node:path");
78086
78538
  const { resolve: resolve16 } = await import("node:path");
78087
78539
  const resolvedPath = resolve16(filePath);
78088
- const filename = basename8(resolvedPath);
78089
- const ext = extname2(filename).toLowerCase();
78540
+ const filename = basename9(resolvedPath);
78541
+ const ext = extname3(filename).toLowerCase();
78090
78542
  const mimeType = MIME_TYPES[ext];
78091
78543
  if (!mimeType) {
78092
78544
  console.error(`Unsupported file type: ${ext}`);
@@ -79037,7 +79489,7 @@ init_src();
79037
79489
  init_gh_cli();
79038
79490
  import { Type as Type7 } from "typebox";
79039
79491
  import { StringEnum } from "@mariozechner/pi-ai";
79040
- import { resolve as resolve15, basename as basename7, extname, join as join36 } from "node:path";
79492
+ import { resolve as resolve15, basename as basename8, extname as extname2, join as join36 } from "node:path";
79041
79493
  import { readFile as readFile18 } from "node:fs/promises";
79042
79494
  import { existsSync as existsSync30 } from "node:fs";
79043
79495
  import { spawn as spawn4 } from "node:child_process";
@@ -79358,8 +79810,8 @@ Column: triage
79358
79810
  }),
79359
79811
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
79360
79812
  const filePath = resolve15(ctx.cwd, params.path.replace(/^@/, ""));
79361
- const filename = basename7(filePath);
79362
- const ext = extname(filename).toLowerCase();
79813
+ const filename = basename8(filePath);
79814
+ const ext = extname2(filename).toLowerCase();
79363
79815
  const mimeType = MIME_TYPES2[ext];
79364
79816
  if (!mimeType) {
79365
79817
  throw new Error(