@runfusion/fusion 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/bin.js +2685 -585
  2. package/dist/client/.vite/manifest.json +61 -61
  3. package/dist/client/assets/{AgentDetailView-CFX3jxfA.js → AgentDetailView-T_AHPxcG.js} +1 -1
  4. package/dist/client/assets/{AgentPermissionPolicyEditor-4tnYHS0-.js → AgentPermissionPolicyEditor-niLSqGAE.js} +1 -1
  5. package/dist/client/assets/{AgentsView-DnNWfwiY.js → AgentsView-CQhaLHz_.js} +3 -3
  6. package/dist/client/assets/{ChatView-rwl2O-s0.js → ChatView-B60kYfdk.js} +1 -1
  7. package/dist/client/assets/{DevServerView-C9fEzcFQ.js → DevServerView-0Gb44WOb.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-Cwyr0Sl4.js → DirectoryPicker-BSQA0XM9.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-gDWTMU2w.js → DocumentsView-XZbaqEFF.js} +1 -1
  10. package/dist/client/assets/{EvalsView-Cw7D1x0W.js → EvalsView-CJNtXxX9.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-8yeOcqbX.js → ExperimentalAgentOnboardingModal-DR8E3i36.js} +1 -1
  12. package/dist/client/assets/GoalsView-CN1zQ_Ax.js +1 -0
  13. package/dist/client/assets/GoalsView-CtWi0fxE.css +1 -0
  14. package/dist/client/assets/{InsightsView-IHHikd5n.js → InsightsView-D_jRaMyn.js} +1 -1
  15. package/dist/client/assets/{MemoryView-pii3HOba.js → MemoryView-D90vdPnp.js} +1 -1
  16. package/dist/client/assets/{NodesView-CoDK14B3.js → NodesView-C1zuedAe.js} +1 -1
  17. package/dist/client/assets/{PiExtensionsManager-vH7EJ5JD.js → PiExtensionsManager-DBJDDVfM.js} +1 -1
  18. package/dist/client/assets/{PluginManager-PbkFQ0Nu.js → PluginManager-Ceg64P2Z.js} +1 -1
  19. package/dist/client/assets/{ResearchView-Dp4g_BWq.js → ResearchView-XRwWdczc.js} +1 -1
  20. package/dist/client/assets/{SecretsView-Q8wES8_N.js → SecretsView-TCrKyCme.js} +1 -1
  21. package/dist/client/assets/{SettingsModal-127YAB8c.js → SettingsModal-B8fRuWdQ.js} +4 -4
  22. package/dist/client/assets/{SettingsModal-DdUztATB.js → SettingsModal-C1TczsEr.js} +1 -1
  23. package/dist/client/assets/{SetupWizardModal-BSAFiQpJ.js → SetupWizardModal-D6moSTT3.js} +1 -1
  24. package/dist/client/assets/{SkillsView-Bj8KQQXB.js → SkillsView-BDHLPVBi.js} +1 -1
  25. package/dist/client/assets/{StashRecoveryView-DvKN_b_k.js → StashRecoveryView-DZGa591R.js} +1 -1
  26. package/dist/client/assets/{TodoView-C-cabt2D.js → TodoView-nr_SnCJv.js} +1 -1
  27. package/dist/client/assets/{dashboard-view-DrHb8VOl.js → dashboard-view-0XeZ1FTR.js} +1 -1
  28. package/dist/client/assets/{folder-open-CYVKFKAx.js → folder-open-iURwQbEg.js} +1 -1
  29. package/dist/client/assets/index-CjuQj4qe.js +742 -0
  30. package/dist/client/assets/index-DmrpDn7c.css +1 -0
  31. package/dist/client/assets/{upload-eQh-mH3-.js → upload-Beqg4S4L.js} +1 -1
  32. package/dist/client/assets/{users-B5upejXg.js → users-C2o86ZCR.js} +1 -1
  33. package/dist/client/index.html +2 -2
  34. package/dist/client/version.json +1 -1
  35. package/dist/droid-cli/index.ts +1 -1
  36. package/dist/droid-cli/package.json +3 -3
  37. package/dist/droid-cli/src/__tests__/thinking-config.test.ts +1 -1
  38. package/dist/extension.js +1928 -287
  39. package/dist/pi-claude-cli/index.ts +3 -3
  40. package/dist/pi-claude-cli/package.json +3 -3
  41. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +3 -3
  42. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +3 -3
  43. package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +1 -1
  44. package/dist/pi-claude-cli/src/event-bridge.ts +2 -2
  45. package/dist/pi-claude-cli/src/provider.ts +1 -1
  46. package/dist/pi-claude-cli/src/thinking-config.ts +1 -1
  47. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  48. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  50. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +34607 -33421
  51. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  52. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  53. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  54. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  55. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  56. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  57. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  58. package/package.json +7 -7
  59. package/skill/fusion/SKILL.md +1 -0
  60. package/skill/fusion/references/cli-commands.md +3 -0
  61. package/skill/fusion/references/extension-tools.md +43 -0
  62. package/skill/fusion/references/fusion-capabilities.md +4 -0
  63. package/dist/client/assets/GoalsView-Cg136Jcn.css +0 -1
  64. package/dist/client/assets/GoalsView-kRIhgAA6.js +0 -1
  65. package/dist/client/assets/index-Cuh_Pqv0.css +0 -1
  66. package/dist/client/assets/index-Cze5qbME.js +0 -742
package/dist/extension.js CHANGED
@@ -328,6 +328,7 @@ var init_settings_schema = __esm({
328
328
  ],
329
329
  prerebaseDivergenceThreshold: 50,
330
330
  mergeConflictStrategy: "smart-prefer-main",
331
+ merger: { mode: "ai", maxReviewPasses: 3 },
331
332
  mergeDiffVolumeMinLines: void 0,
332
333
  mergeDiffVolumeThreshold: void 0,
333
334
  mergeDiffVolumeAllowlist: void 0,
@@ -407,6 +408,7 @@ var init_settings_schema = __esm({
407
408
  autoUpdatePrStatus: false,
408
409
  githubCommentOnDone: false,
409
410
  githubCommentTemplate: void 0,
411
+ githubCloseSourceIssueOnDone: false,
410
412
  githubTrackingEnabledByDefault: false,
411
413
  githubTrackingDefaultRepo: void 0,
412
414
  githubTrackingDedupEnabled: true,
@@ -1271,6 +1273,9 @@ function normalizePostMergeAuditMode(value) {
1271
1273
  function normalizeMergeAuditAutoRecovery(value) {
1272
1274
  return typeof value === "string" && MERGE_AUDIT_AUTO_RECOVERY_MODES.includes(value) ? value : "ai-assisted";
1273
1275
  }
1276
+ function normalizeMergerMode(value) {
1277
+ return typeof value === "string" && MERGER_MODES.includes(value) ? value : "ai";
1278
+ }
1274
1279
  function normalizeAutoRecovery(value) {
1275
1280
  const fallback = { mode: "deterministic-only", maxRetries: 3 };
1276
1281
  if (!value || typeof value !== "object") return fallback;
@@ -1577,7 +1582,7 @@ function validateMessageMetadata(metadata) {
1577
1582
  throw new Error("metadata.wakeRecipient must be a boolean");
1578
1583
  }
1579
1584
  }
1580
- var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, warnedLegacyCwdMain, MERGE_ADVANCE_AUTO_SYNC_MODES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, DUPLICATE_OF_METADATA_KEY, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, SECRET_ACCESS_POLICIES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
1585
+ var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, warnedLegacyCwdMain, MERGE_ADVANCE_AUTO_SYNC_MODES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, MERGER_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, DUPLICATE_OF_METADATA_KEY, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, SECRET_ACCESS_POLICIES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
1581
1586
  var init_types = __esm({
1582
1587
  "../core/src/types.ts"() {
1583
1588
  "use strict";
@@ -1662,6 +1667,7 @@ var init_types = __esm({
1662
1667
  ];
1663
1668
  POST_MERGE_AUDIT_MODES = ["block", "warn", "off"];
1664
1669
  MERGE_AUDIT_AUTO_RECOVERY_MODES = ["deterministic-only", "programmatic", "ai-assisted", "off"];
1670
+ MERGER_MODES = ["ai", "deterministic"];
1665
1671
  AUTO_RECOVERY_MODES = ["off", "deterministic-only", "programmatic", "ai-assisted"];
1666
1672
  NOTIFICATION_EVENTS = [
1667
1673
  "in-review",
@@ -4666,7 +4672,7 @@ var init_db = __esm({
4666
4672
  DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
4667
4673
  DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
4668
4674
  DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
4669
- SCHEMA_VERSION = 91;
4675
+ SCHEMA_VERSION = 92;
4670
4676
  SCHEMA_SQL = `
4671
4677
  -- Tasks table with JSON columns for nested data
4672
4678
  CREATE TABLE IF NOT EXISTS tasks (
@@ -5219,6 +5225,17 @@ CREATE TABLE IF NOT EXISTS missions (
5219
5225
  updatedAt TEXT NOT NULL
5220
5226
  );
5221
5227
 
5228
+ -- Goals table (strategic intent across mission timelines)
5229
+ CREATE TABLE IF NOT EXISTS goals (
5230
+ id TEXT PRIMARY KEY,
5231
+ title TEXT NOT NULL,
5232
+ description TEXT,
5233
+ status TEXT NOT NULL,
5234
+ createdAt TEXT NOT NULL,
5235
+ updatedAt TEXT NOT NULL
5236
+ );
5237
+ CREATE INDEX IF NOT EXISTS idxGoalsStatus ON goals(status);
5238
+
5222
5239
  -- Milestones table (phases within a mission)
5223
5240
  CREATE TABLE IF NOT EXISTS milestones (
5224
5241
  id TEXT PRIMARY KEY,
@@ -7531,6 +7548,24 @@ This means a caller passed a .fusion directory where a project root was expected
7531
7548
  this.addColumnIfMissing("missions", "baseBranch", "TEXT");
7532
7549
  });
7533
7550
  }
7551
+ if (version < 92) {
7552
+ this.applyMigration(92, () => {
7553
+ this.db.exec(`
7554
+ CREATE TABLE IF NOT EXISTS goals (
7555
+ id TEXT PRIMARY KEY,
7556
+ title TEXT NOT NULL,
7557
+ description TEXT,
7558
+ status TEXT NOT NULL,
7559
+ createdAt TEXT NOT NULL,
7560
+ updatedAt TEXT NOT NULL
7561
+ )
7562
+ `);
7563
+ this.db.exec(`
7564
+ CREATE INDEX IF NOT EXISTS idxGoalsStatus
7565
+ ON goals(status)
7566
+ `);
7567
+ });
7568
+ }
7534
7569
  }
7535
7570
  /**
7536
7571
  * Run a single migration step inside a transaction and bump the version.
@@ -18654,6 +18689,158 @@ var init_todo_store = __esm({
18654
18689
  }
18655
18690
  });
18656
18691
 
18692
+ // ../core/src/goal-types.ts
18693
+ var ACTIVE_GOAL_LIMIT, ActiveGoalLimitExceededError;
18694
+ var init_goal_types = __esm({
18695
+ "../core/src/goal-types.ts"() {
18696
+ "use strict";
18697
+ ACTIVE_GOAL_LIMIT = 5;
18698
+ ActiveGoalLimitExceededError = class extends Error {
18699
+ constructor(limit, currentActive) {
18700
+ super(`Active goal limit exceeded: ${currentActive}/${limit}`);
18701
+ this.limit = limit;
18702
+ this.currentActive = currentActive;
18703
+ this.name = "ActiveGoalLimitExceededError";
18704
+ }
18705
+ code = "ACTIVE_GOAL_LIMIT_EXCEEDED";
18706
+ };
18707
+ }
18708
+ });
18709
+
18710
+ // ../core/src/goal-store.ts
18711
+ import { EventEmitter as EventEmitter10 } from "node:events";
18712
+ var GoalStore;
18713
+ var init_goal_store = __esm({
18714
+ "../core/src/goal-store.ts"() {
18715
+ "use strict";
18716
+ init_goal_types();
18717
+ GoalStore = class extends EventEmitter10 {
18718
+ constructor(_fusionDir, db) {
18719
+ super();
18720
+ this.db = db;
18721
+ }
18722
+ idSequence = 0;
18723
+ createGoal(input) {
18724
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18725
+ const goal = this.db.transactionImmediate(() => {
18726
+ const activeCountRow = this.db.prepare("SELECT COUNT(*) as count FROM goals WHERE status = 'active'").get();
18727
+ const currentActive = activeCountRow?.count ?? 0;
18728
+ if (currentActive >= ACTIVE_GOAL_LIMIT) {
18729
+ throw new ActiveGoalLimitExceededError(ACTIVE_GOAL_LIMIT, currentActive);
18730
+ }
18731
+ const created = {
18732
+ id: this.generateGoalId(),
18733
+ title: input.title,
18734
+ description: input.description,
18735
+ status: "active",
18736
+ createdAt: now,
18737
+ updatedAt: now
18738
+ };
18739
+ this.db.prepare(
18740
+ "INSERT INTO goals (id, title, description, status, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)"
18741
+ ).run(created.id, created.title, created.description ?? null, created.status, created.createdAt, created.updatedAt);
18742
+ return created;
18743
+ });
18744
+ this.db.bumpLastModified();
18745
+ this.emit("goal:created", goal);
18746
+ return goal;
18747
+ }
18748
+ updateGoal(id, input) {
18749
+ const existing = this.getGoal(id);
18750
+ if (!existing) {
18751
+ throw new Error(`Goal ${id} not found`);
18752
+ }
18753
+ const updated = {
18754
+ ...existing,
18755
+ title: input.title ?? existing.title,
18756
+ description: input.description ?? existing.description,
18757
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18758
+ };
18759
+ this.db.prepare("UPDATE goals SET title = ?, description = ?, updatedAt = ? WHERE id = ?").run(updated.title, updated.description ?? null, updated.updatedAt, id);
18760
+ this.db.bumpLastModified();
18761
+ this.emit("goal:updated", updated);
18762
+ return updated;
18763
+ }
18764
+ archiveGoal(id) {
18765
+ const existing = this.getGoal(id);
18766
+ if (!existing) {
18767
+ throw new Error(`Goal ${id} not found`);
18768
+ }
18769
+ if (existing.status === "archived") {
18770
+ this.emit("goal:updated", existing);
18771
+ return existing;
18772
+ }
18773
+ const updated = {
18774
+ ...existing,
18775
+ status: "archived",
18776
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18777
+ };
18778
+ this.db.prepare("UPDATE goals SET status = 'archived', updatedAt = ? WHERE id = ?").run(updated.updatedAt, id);
18779
+ this.db.bumpLastModified();
18780
+ this.emit("goal:updated", updated);
18781
+ return updated;
18782
+ }
18783
+ unarchiveGoal(id) {
18784
+ const { goal, changed } = this.db.transactionImmediate(() => {
18785
+ const existing = this.getGoal(id);
18786
+ if (!existing) {
18787
+ throw new Error(`Goal ${id} not found`);
18788
+ }
18789
+ if (existing.status === "active") {
18790
+ return { goal: existing, changed: false };
18791
+ }
18792
+ const activeCountRow = this.db.prepare("SELECT COUNT(*) as count FROM goals WHERE status = 'active'").get();
18793
+ const currentActive = activeCountRow?.count ?? 0;
18794
+ if (currentActive >= ACTIVE_GOAL_LIMIT) {
18795
+ throw new ActiveGoalLimitExceededError(ACTIVE_GOAL_LIMIT, currentActive);
18796
+ }
18797
+ const updated = {
18798
+ ...existing,
18799
+ status: "active",
18800
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18801
+ };
18802
+ this.db.prepare("UPDATE goals SET status = 'active', updatedAt = ? WHERE id = ?").run(updated.updatedAt, id);
18803
+ return { goal: updated, changed: true };
18804
+ });
18805
+ if (!changed) {
18806
+ return goal;
18807
+ }
18808
+ this.db.bumpLastModified();
18809
+ this.emit("goal:updated", goal);
18810
+ return goal;
18811
+ }
18812
+ listGoals(filter) {
18813
+ const rows = filter?.status ? this.db.prepare("SELECT id, title, description, status, createdAt, updatedAt FROM goals WHERE status = ? ORDER BY createdAt ASC").all(filter.status) : this.db.prepare("SELECT id, title, description, status, createdAt, updatedAt FROM goals ORDER BY createdAt ASC").all();
18814
+ return rows.map((row) => this.toGoal(row));
18815
+ }
18816
+ getGoal(id) {
18817
+ const row = this.db.prepare("SELECT id, title, description, status, createdAt, updatedAt FROM goals WHERE id = ?").get(id);
18818
+ if (!row) {
18819
+ return null;
18820
+ }
18821
+ return this.toGoal(row);
18822
+ }
18823
+ toGoal(row) {
18824
+ return {
18825
+ id: row.id,
18826
+ title: row.title,
18827
+ description: row.description ?? void 0,
18828
+ status: row.status,
18829
+ createdAt: row.createdAt,
18830
+ updatedAt: row.updatedAt
18831
+ };
18832
+ }
18833
+ generateGoalId() {
18834
+ const timestamp = Date.now().toString(36).toUpperCase();
18835
+ this.idSequence += 1;
18836
+ const sequence = this.idSequence.toString(36).toUpperCase().padStart(4, "0");
18837
+ const random = Math.random().toString(36).substring(2, 6).toUpperCase();
18838
+ return `G-${timestamp}-${sequence}-${random}`;
18839
+ }
18840
+ };
18841
+ }
18842
+ });
18843
+
18657
18844
  // ../core/src/eval-types.ts
18658
18845
  function normalizeEvalFollowUpText(value) {
18659
18846
  return value.toLowerCase().replace(/\s+/g, " ").replace(/[^a-z0-9 ]/g, "").trim();
@@ -18732,7 +18919,7 @@ var init_eval_types = __esm({
18732
18919
  });
18733
18920
 
18734
18921
  // ../core/src/eval-store.ts
18735
- import { EventEmitter as EventEmitter10 } from "node:events";
18922
+ import { EventEmitter as EventEmitter11 } from "node:events";
18736
18923
  import { randomUUID as randomUUID9 } from "node:crypto";
18737
18924
  function generateRunId3() {
18738
18925
  return `ER-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`;
@@ -18828,7 +19015,7 @@ var init_eval_store = __esm({
18828
19015
  this.name = "EvalLifecycleError";
18829
19016
  }
18830
19017
  };
18831
- EvalStore = class extends EventEmitter10 {
19018
+ EvalStore = class extends EventEmitter11 {
18832
19019
  constructor(db) {
18833
19020
  super();
18834
19021
  this.db = db;
@@ -19788,7 +19975,7 @@ var require_registry = __commonJS({
19788
19975
  setTimeout(send, Math.random() * 250);
19789
19976
  }
19790
19977
  announce(server, service) {
19791
- var delay4 = 1e3;
19978
+ var delay5 = 1e3;
19792
19979
  var packet = service.records();
19793
19980
  server.register(packet);
19794
19981
  const broadcast = () => {
@@ -19800,9 +19987,9 @@ var require_registry = __commonJS({
19800
19987
  service.published = true;
19801
19988
  service.emit("up");
19802
19989
  }
19803
- delay4 = delay4 * REANNOUNCE_FACTOR;
19804
- if (delay4 < REANNOUNCE_MAX_MS && !service.destroyed) {
19805
- setTimeout(broadcast, delay4).unref();
19990
+ delay5 = delay5 * REANNOUNCE_FACTOR;
19991
+ if (delay5 < REANNOUNCE_MAX_MS && !service.destroyed) {
19992
+ setTimeout(broadcast, delay5).unref();
19806
19993
  }
19807
19994
  });
19808
19995
  };
@@ -22628,7 +22815,7 @@ var require_dist = __commonJS({
22628
22815
  });
22629
22816
 
22630
22817
  // ../core/src/node-discovery.ts
22631
- import { EventEmitter as EventEmitter11 } from "node:events";
22818
+ import { EventEmitter as EventEmitter12 } from "node:events";
22632
22819
  import os from "node:os";
22633
22820
  var import_bonjour_service, DEFAULT_DISCOVERY_CONFIG, STALE_CLEANUP_INTERVAL_MS, FUSION_VERSION, NodeDiscovery;
22634
22821
  var init_node_discovery = __esm({
@@ -22644,7 +22831,7 @@ var init_node_discovery = __esm({
22644
22831
  };
22645
22832
  STALE_CLEANUP_INTERVAL_MS = 6e4;
22646
22833
  FUSION_VERSION = "0.1.0";
22647
- NodeDiscovery = class extends EventEmitter11 {
22834
+ NodeDiscovery = class extends EventEmitter12 {
22648
22835
  config;
22649
22836
  bonjour = null;
22650
22837
  broadcastService = null;
@@ -23180,7 +23367,7 @@ var init_project_identity = __esm({
23180
23367
  });
23181
23368
 
23182
23369
  // ../core/src/central-core.ts
23183
- import { EventEmitter as EventEmitter12 } from "node:events";
23370
+ import { EventEmitter as EventEmitter13 } from "node:events";
23184
23371
  import { createHash as createHash5, randomUUID as randomUUID10 } from "node:crypto";
23185
23372
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
23186
23373
  import { mkdir as mkdir4 } from "node:fs/promises";
@@ -23198,7 +23385,7 @@ var init_central_core = __esm({
23198
23385
  init_system_metrics();
23199
23386
  init_shared_mesh_state();
23200
23387
  init_project_identity();
23201
- CentralCore = class extends EventEmitter12 {
23388
+ CentralCore = class extends EventEmitter13 {
23202
23389
  db = null;
23203
23390
  globalDir;
23204
23391
  initialized = false;
@@ -25369,10 +25556,10 @@ var init_central_core = __esm({
25369
25556
  */
25370
25557
  async generateProjectName(projectPath) {
25371
25558
  try {
25372
- const { execFile: execFile19 } = await import("node:child_process");
25373
- const { promisify: promisify50 } = await import("node:util");
25374
- const execFileAsync14 = promisify50(execFile19);
25375
- const { stdout } = await execFileAsync14(
25559
+ const { execFile: execFile20 } = await import("node:child_process");
25560
+ const { promisify: promisify51 } = await import("node:util");
25561
+ const execFileAsync15 = promisify51(execFile20);
25562
+ const { stdout } = await execFileAsync15(
25376
25563
  "git",
25377
25564
  ["remote", "get-url", "origin"],
25378
25565
  { cwd: projectPath, timeout: 5e3 }
@@ -26076,10 +26263,10 @@ var init_migration = __esm({
26076
26263
  return basename5(projectPath);
26077
26264
  }
26078
26265
  try {
26079
- const { execFile: execFile19 } = await import("node:child_process");
26080
- const { promisify: promisify50 } = await import("node:util");
26081
- const execFileAsync14 = promisify50(execFile19);
26082
- const { stdout } = await execFileAsync14(
26266
+ const { execFile: execFile20 } = await import("node:child_process");
26267
+ const { promisify: promisify51 } = await import("node:util");
26268
+ const execFileAsync15 = promisify51(execFile20);
26269
+ const { stdout } = await execFileAsync15(
26083
26270
  "git",
26084
26271
  ["remote", "get-url", "origin"],
26085
26272
  { cwd: projectPath, timeout: 1e3 }
@@ -36723,7 +36910,7 @@ var automation_store_exports = {};
36723
36910
  __export(automation_store_exports, {
36724
36911
  AutomationStore: () => AutomationStore
36725
36912
  });
36726
- import { EventEmitter as EventEmitter13 } from "node:events";
36913
+ import { EventEmitter as EventEmitter14 } from "node:events";
36727
36914
  import { join as join16 } from "node:path";
36728
36915
  import { randomUUID as randomUUID12 } from "node:crypto";
36729
36916
  var import_cron_parser, CRON_TIMEZONE, AutomationStore;
@@ -36735,7 +36922,7 @@ var init_automation_store = __esm({
36735
36922
  init_db();
36736
36923
  init_project_root_guard();
36737
36924
  CRON_TIMEZONE = "UTC";
36738
- AutomationStore = class _AutomationStore extends EventEmitter13 {
36925
+ AutomationStore = class _AutomationStore extends EventEmitter14 {
36739
36926
  constructor(rootDir, options) {
36740
36927
  super();
36741
36928
  this.rootDir = rootDir;
@@ -37827,13 +38014,13 @@ async function searchWithQmd(rootDir, options) {
37827
38014
  const command = "qmd";
37828
38015
  const limit = Math.max(1, Math.min(options.limit ?? 5, 20));
37829
38016
  try {
37830
- const { execFile: execFile19 } = await import("node:child_process");
37831
- const { promisify: promisify50 } = await import("node:util");
37832
- const execFileAsync14 = promisify50(execFile19);
37833
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync14);
38017
+ const { execFile: execFile20 } = await import("node:child_process");
38018
+ const { promisify: promisify51 } = await import("node:util");
38019
+ const execFileAsync15 = promisify51(execFile20);
38020
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync15);
37834
38021
  scheduleQmdProjectMemoryRefresh(rootDir);
37835
38022
  const args = buildQmdSearchArgs(rootDir, options);
37836
- const { stdout } = await execFileAsync14(command, args, {
38023
+ const { stdout } = await execFileAsync15(command, args, {
37837
38024
  cwd: rootDir,
37838
38025
  timeout: 4e3,
37839
38026
  maxBuffer: 1024 * 1024
@@ -37858,12 +38045,12 @@ async function searchWithQmd(rootDir, options) {
37858
38045
  return [];
37859
38046
  }
37860
38047
  }
37861
- async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync14) {
38048
+ async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync15) {
37862
38049
  const collectionName = qmdMemoryCollectionName(rootDir);
37863
38050
  const memoryDir = memoryWorkspacePath(rootDir);
37864
38051
  await mkdir6(memoryDir, { recursive: true });
37865
38052
  try {
37866
- await execFileAsync14("qmd", buildQmdCollectionAddArgs(rootDir), {
38053
+ await execFileAsync15("qmd", buildQmdCollectionAddArgs(rootDir), {
37867
38054
  cwd: rootDir,
37868
38055
  timeout: 4e3,
37869
38056
  maxBuffer: 512 * 1024
@@ -37879,9 +38066,9 @@ ${stderr}`)) {
37879
38066
  return collectionName;
37880
38067
  }
37881
38068
  async function getDefaultExecFileAsync() {
37882
- const { execFile: execFile19 } = await import("node:child_process");
37883
- const { promisify: promisify50 } = await import("node:util");
37884
- return promisify50(execFile19);
38069
+ const { execFile: execFile20 } = await import("node:child_process");
38070
+ const { promisify: promisify51 } = await import("node:util");
38071
+ return promisify51(execFile20);
37885
38072
  }
37886
38073
  async function refreshQmdProjectMemoryIndex(rootDir, options) {
37887
38074
  const key = resolve8(rootDir);
@@ -37896,14 +38083,14 @@ async function refreshQmdProjectMemoryIndex(rootDir, options) {
37896
38083
  }
37897
38084
  }
37898
38085
  const promise = (async () => {
37899
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37900
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync14);
37901
- await execFileAsync14("qmd", ["update"], {
38086
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38087
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync15);
38088
+ await execFileAsync15("qmd", ["update"], {
37902
38089
  cwd: rootDir,
37903
38090
  timeout: 3e4,
37904
38091
  maxBuffer: 1024 * 1024
37905
38092
  });
37906
- await execFileAsync14("qmd", ["embed"], {
38093
+ await execFileAsync15("qmd", ["embed"], {
37907
38094
  cwd: rootDir,
37908
38095
  timeout: 12e4,
37909
38096
  maxBuffer: 1024 * 1024
@@ -37939,12 +38126,12 @@ async function refreshQmdAgentMemoryIndex(rootDir, agentId, options) {
37939
38126
  }
37940
38127
  }
37941
38128
  const promise = (async () => {
37942
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38129
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37943
38130
  const { agentMemoryWorkspacePath: agentMemoryWorkspacePath2 } = await Promise.resolve().then(() => (init_memory_dreams(), memory_dreams_exports));
37944
38131
  const workspacePath = agentMemoryWorkspacePath2(rootDir, agentId);
37945
38132
  await mkdir6(workspacePath, { recursive: true });
37946
38133
  try {
37947
- await execFileAsync14("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
38134
+ await execFileAsync15("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
37948
38135
  cwd: rootDir,
37949
38136
  timeout: 4e3,
37950
38137
  maxBuffer: 512 * 1024
@@ -37957,8 +38144,8 @@ ${stderr}`)) {
37957
38144
  throw err;
37958
38145
  }
37959
38146
  }
37960
- await execFileAsync14("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
37961
- await execFileAsync14("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
38147
+ await execFileAsync15("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
38148
+ await execFileAsync15("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
37962
38149
  })();
37963
38150
  qmdAgentRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
37964
38151
  try {
@@ -37979,8 +38166,8 @@ function scheduleQmdAgentMemoryRefresh(rootDir, agentId) {
37979
38166
  }
37980
38167
  async function isQmdAvailable() {
37981
38168
  try {
37982
- const execFileAsync14 = await getDefaultExecFileAsync();
37983
- await execFileAsync14("qmd", ["--help"], {
38169
+ const execFileAsync15 = await getDefaultExecFileAsync();
38170
+ await execFileAsync15("qmd", ["--help"], {
37984
38171
  timeout: 3e3,
37985
38172
  maxBuffer: 128 * 1024
37986
38173
  });
@@ -37990,12 +38177,12 @@ async function isQmdAvailable() {
37990
38177
  }
37991
38178
  }
37992
38179
  async function installQmd(options) {
37993
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38180
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37994
38181
  const [command, ...args] = QMD_INSTALL_COMMAND.split(" ");
37995
38182
  if (!command || args.length === 0) {
37996
38183
  throw new MemoryBackendError("BACKEND_UNAVAILABLE", "qmd install command is not configured", "qmd");
37997
38184
  }
37998
- await execFileAsync14(command, args, {
38185
+ await execFileAsync15(command, args, {
37999
38186
  timeout: 12e4,
38000
38187
  maxBuffer: 1024 * 1024
38001
38188
  });
@@ -39646,7 +39833,7 @@ var init_task_id_integrity = __esm({
39646
39833
  });
39647
39834
 
39648
39835
  // ../core/src/store.ts
39649
- import { EventEmitter as EventEmitter14 } from "node:events";
39836
+ import { EventEmitter as EventEmitter15 } from "node:events";
39650
39837
  import { randomUUID as randomUUID15 } from "node:crypto";
39651
39838
  import { mkdir as mkdir7, readdir as readdir5, readFile as readFile8, writeFile as writeFile6, rename as rename4, unlink as unlink3 } from "node:fs/promises";
39652
39839
  import { join as join19 } from "node:path";
@@ -39978,6 +40165,7 @@ var init_store = __esm({
39978
40165
  init_research_store();
39979
40166
  init_experiment_session_store();
39980
40167
  init_todo_store();
40168
+ init_goal_store();
39981
40169
  init_eval_store();
39982
40170
  init_migration();
39983
40171
  init_central_core();
@@ -40171,7 +40359,7 @@ var init_store = __esm({
40171
40359
  this.name = "HandoffInvariantViolationError";
40172
40360
  }
40173
40361
  };
40174
- TaskStore = class _TaskStore extends EventEmitter14 {
40362
+ TaskStore = class _TaskStore extends EventEmitter15 {
40175
40363
  constructor(rootDir, globalSettingsDir, options) {
40176
40364
  super();
40177
40365
  this.rootDir = rootDir;
@@ -40303,6 +40491,8 @@ var init_store = __esm({
40303
40491
  experimentSessionStore = null;
40304
40492
  /** Cached TodoStore instance */
40305
40493
  todoStore = null;
40494
+ /** Cached GoalStore instance */
40495
+ goalStore = null;
40306
40496
  /** Cached EvalStore instance */
40307
40497
  evalStore = null;
40308
40498
  /** Cached SecretsStore instance */
@@ -42686,9 +42876,9 @@ ${outcome}`;
42686
42876
  if (validation.invalid.length > 0) {
42687
42877
  if (this.isWatching) this.taskCache.delete(id);
42688
42878
  this.deleteTaskById(id);
42689
- const { rm: rm9 } = await import("node:fs/promises");
42879
+ const { rm: rm10 } = await import("node:fs/promises");
42690
42880
  if (existsSync15(dir)) {
42691
- await rm9(dir, { recursive: true, force: true });
42881
+ await rm10(dir, { recursive: true, force: true });
42692
42882
  }
42693
42883
  throw new InvalidFileScopeError(id, validation.invalid);
42694
42884
  }
@@ -42783,10 +42973,10 @@ ${outcome}`;
42783
42973
  });
42784
42974
  if (this.isWatching) this.taskCache.delete(task.id);
42785
42975
  this.deleteTaskById(task.id);
42786
- const { rm: rm9 } = await import("node:fs/promises");
42976
+ const { rm: rm10 } = await import("node:fs/promises");
42787
42977
  const taskDir = this.taskDir(task.id);
42788
42978
  if (existsSync15(taskDir)) {
42789
- await rm9(taskDir, { recursive: true, force: true });
42979
+ await rm10(taskDir, { recursive: true, force: true });
42790
42980
  }
42791
42981
  throw new TombstonedTaskResurrectionError(
42792
42982
  tombstonedMatch.id,
@@ -45803,8 +45993,8 @@ ${newTask.description}
45803
45993
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
45804
45994
  this.db.bumpLastModified();
45805
45995
  });
45806
- const { rm: rm9 } = await import("node:fs/promises");
45807
- await rm9(dir, { recursive: true, force: true });
45996
+ const { rm: rm10 } = await import("node:fs/promises");
45997
+ await rm10(dir, { recursive: true, force: true });
45808
45998
  if (this.isWatching) {
45809
45999
  this.taskCache.delete(id);
45810
46000
  }
@@ -47213,14 +47403,14 @@ ${newTask.description}
47213
47403
  if (rows.length === 0) {
47214
47404
  return;
47215
47405
  }
47216
- const { rm: rm9 } = await import("node:fs/promises");
47406
+ const { rm: rm10 } = await import("node:fs/promises");
47217
47407
  for (const row of rows) {
47218
47408
  const task = this.rowToTask(row);
47219
47409
  const archivedAt = task.columnMovedAt ?? task.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
47220
47410
  const entry = await this.taskToArchiveEntry(task, archivedAt);
47221
47411
  this.archiveDb.upsert(entry);
47222
47412
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
47223
- await rm9(this.taskDir(task.id), { recursive: true, force: true });
47413
+ await rm10(this.taskDir(task.id), { recursive: true, force: true });
47224
47414
  if (this.isWatching) {
47225
47415
  this.taskCache.delete(task.id);
47226
47416
  }
@@ -47247,8 +47437,8 @@ ${newTask.description}
47247
47437
  this.archiveDb.upsert(entry);
47248
47438
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
47249
47439
  this.db.bumpLastModified();
47250
- const { rm: rm9 } = await import("node:fs/promises");
47251
- await rm9(dir, { recursive: true, force: true });
47440
+ const { rm: rm10 } = await import("node:fs/promises");
47441
+ await rm10(dir, { recursive: true, force: true });
47252
47442
  if (this.isWatching) {
47253
47443
  this.taskCache.delete(task.id);
47254
47444
  }
@@ -47982,6 +48172,16 @@ ${notificationsSection}`;
47982
48172
  }
47983
48173
  return this.todoStore;
47984
48174
  }
48175
+ /**
48176
+ * Get the GoalStore instance for project-scoped goals operations.
48177
+ * Lazily initializes the GoalStore on first access.
48178
+ */
48179
+ getGoalStore() {
48180
+ if (!this.goalStore) {
48181
+ this.goalStore = new GoalStore(this.fusionDir, this.db);
48182
+ }
48183
+ return this.goalStore;
48184
+ }
47985
48185
  /**
47986
48186
  * Get the EvalStore instance for eval run and task result operations.
47987
48187
  * Lazily initializes the EvalStore on first access.
@@ -49179,8 +49379,11 @@ function describeAbortReason(reason) {
49179
49379
  if (typeof reason === "string") return reason;
49180
49380
  return "aborted";
49181
49381
  }
49382
+ function shouldAutoAppendJsonFlag(args) {
49383
+ return args[0] !== "api" && !args.includes("--json");
49384
+ }
49182
49385
  function runGhJson(args, cwd) {
49183
- const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
49386
+ const jsonArgs = shouldAutoAppendJsonFlag(args) ? [...args, "--json"] : args;
49184
49387
  const output = runGh(jsonArgs, cwd);
49185
49388
  try {
49186
49389
  return JSON.parse(output);
@@ -49189,7 +49392,7 @@ function runGhJson(args, cwd) {
49189
49392
  }
49190
49393
  }
49191
49394
  async function runGhJsonAsync(args, cwdOrOptions) {
49192
- const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
49395
+ const jsonArgs = shouldAutoAppendJsonFlag(args) ? [...args, "--json"] : args;
49193
49396
  const output = await runGhAsync(jsonArgs, cwdOrOptions);
49194
49397
  try {
49195
49398
  return JSON.parse(output);
@@ -49789,7 +49992,7 @@ var routine_store_exports = {};
49789
49992
  __export(routine_store_exports, {
49790
49993
  RoutineStore: () => RoutineStore
49791
49994
  });
49792
- import { EventEmitter as EventEmitter15 } from "node:events";
49995
+ import { EventEmitter as EventEmitter16 } from "node:events";
49793
49996
  import { randomUUID as randomUUID16 } from "node:crypto";
49794
49997
  var import_cron_parser2, CRON_TIMEZONE2, RoutineStore;
49795
49998
  var init_routine_store = __esm({
@@ -49800,7 +50003,7 @@ var init_routine_store = __esm({
49800
50003
  init_routine();
49801
50004
  init_project_root_guard();
49802
50005
  CRON_TIMEZONE2 = "UTC";
49803
- RoutineStore = class _RoutineStore extends EventEmitter15 {
50006
+ RoutineStore = class _RoutineStore extends EventEmitter16 {
49804
50007
  constructor(rootDir, options) {
49805
50008
  super();
49806
50009
  this.rootDir = rootDir;
@@ -50402,7 +50605,7 @@ import { basename as basename8, dirname as dirname7, extname, isAbsolute as isAb
50402
50605
  import { stat as stat3 } from "node:fs/promises";
50403
50606
  import { copyFile } from "node:fs/promises";
50404
50607
  import { pathToFileURL } from "node:url";
50405
- import { EventEmitter as EventEmitter16 } from "node:events";
50608
+ import { EventEmitter as EventEmitter17 } from "node:events";
50406
50609
  var MINIMUM_FUSION_VERSION, moduleImportVersion, PluginLoader;
50407
50610
  var init_plugin_loader = __esm({
50408
50611
  "../core/src/plugin-loader.ts"() {
@@ -50413,7 +50616,7 @@ var init_plugin_loader = __esm({
50413
50616
  init_plugin_security_scan();
50414
50617
  MINIMUM_FUSION_VERSION = "0.1.0";
50415
50618
  moduleImportVersion = 0;
50416
- PluginLoader = class extends EventEmitter16 {
50619
+ PluginLoader = class extends EventEmitter17 {
50417
50620
  constructor(options) {
50418
50621
  super();
50419
50622
  this.options = options;
@@ -55965,15 +56168,15 @@ var require_fd_slicer = __commonJS({
55965
56168
  var Writable = stream.Writable;
55966
56169
  var PassThrough = stream.PassThrough;
55967
56170
  var Pend = require_pend();
55968
- var EventEmitter34 = __require("events").EventEmitter;
56171
+ var EventEmitter35 = __require("events").EventEmitter;
55969
56172
  exports.createFromBuffer = createFromBuffer;
55970
56173
  exports.createFromFd = createFromFd;
55971
56174
  exports.BufferSlicer = BufferSlicer;
55972
56175
  exports.FdSlicer = FdSlicer;
55973
- util.inherits(FdSlicer, EventEmitter34);
56176
+ util.inherits(FdSlicer, EventEmitter35);
55974
56177
  function FdSlicer(fd, options) {
55975
56178
  options = options || {};
55976
- EventEmitter34.call(this);
56179
+ EventEmitter35.call(this);
55977
56180
  this.fd = fd;
55978
56181
  this.pend = new Pend();
55979
56182
  this.pend.max = 1;
@@ -56117,9 +56320,9 @@ var require_fd_slicer = __commonJS({
56117
56320
  this.destroyed = true;
56118
56321
  this.context.unref();
56119
56322
  };
56120
- util.inherits(BufferSlicer, EventEmitter34);
56323
+ util.inherits(BufferSlicer, EventEmitter35);
56121
56324
  function BufferSlicer(buffer, options) {
56122
- EventEmitter34.call(this);
56325
+ EventEmitter35.call(this);
56123
56326
  options = options || {};
56124
56327
  this.refCount = 0;
56125
56328
  this.buffer = buffer;
@@ -56531,7 +56734,7 @@ var require_yauzl = __commonJS({
56531
56734
  var fd_slicer = require_fd_slicer();
56532
56735
  var crc32 = require_buffer_crc32();
56533
56736
  var util = __require("util");
56534
- var EventEmitter34 = __require("events").EventEmitter;
56737
+ var EventEmitter35 = __require("events").EventEmitter;
56535
56738
  var Transform = __require("stream").Transform;
56536
56739
  var PassThrough = __require("stream").PassThrough;
56537
56740
  var Writable = __require("stream").Writable;
@@ -56663,10 +56866,10 @@ var require_yauzl = __commonJS({
56663
56866
  callback(new Error("end of central directory record signature not found"));
56664
56867
  });
56665
56868
  }
56666
- util.inherits(ZipFile, EventEmitter34);
56869
+ util.inherits(ZipFile, EventEmitter35);
56667
56870
  function ZipFile(reader, centralDirectoryOffset, fileSize, entryCount, comment, autoClose, lazyEntries, decodeStrings, validateEntrySizes, strictFileNames) {
56668
56871
  var self = this;
56669
- EventEmitter34.call(self);
56872
+ EventEmitter35.call(self);
56670
56873
  self.reader = reader;
56671
56874
  self.reader.on("error", function(err) {
56672
56875
  emitError(self, err);
@@ -57027,9 +57230,9 @@ var require_yauzl = __commonJS({
57027
57230
  }
57028
57231
  cb();
57029
57232
  };
57030
- util.inherits(RandomAccessReader, EventEmitter34);
57233
+ util.inherits(RandomAccessReader, EventEmitter35);
57031
57234
  function RandomAccessReader() {
57032
- EventEmitter34.call(this);
57235
+ EventEmitter35.call(this);
57033
57236
  this.refCount = 0;
57034
57237
  }
57035
57238
  RandomAccessReader.prototype.ref = function() {
@@ -57160,11 +57363,11 @@ var require_extract_zip = __commonJS({
57160
57363
  var { createWriteStream, promises: fs6 } = __require("fs");
57161
57364
  var getStream = require_get_stream();
57162
57365
  var path6 = __require("path");
57163
- var { promisify: promisify50 } = __require("util");
57366
+ var { promisify: promisify51 } = __require("util");
57164
57367
  var stream = __require("stream");
57165
57368
  var yauzl = require_yauzl();
57166
- var openZip = promisify50(yauzl.open);
57167
- var pipeline = promisify50(stream.pipeline);
57369
+ var openZip = promisify51(yauzl.open);
57370
+ var pipeline = promisify51(stream.pipeline);
57168
57371
  var Extractor = class {
57169
57372
  constructor(zipPath, opts) {
57170
57373
  this.zipPath = zipPath;
@@ -57246,7 +57449,7 @@ var require_extract_zip = __commonJS({
57246
57449
  await fs6.mkdir(destDir, mkdirOptions);
57247
57450
  if (isDir) return;
57248
57451
  debug("opening read stream", dest);
57249
- const readStream = await promisify50(this.zipfile.openReadStream.bind(this.zipfile))(entry);
57452
+ const readStream = await promisify51(this.zipfile.openReadStream.bind(this.zipfile))(entry);
57250
57453
  if (symlink) {
57251
57454
  const link = await getStream(readStream);
57252
57455
  debug("creating symlink", link, dest);
@@ -68267,14 +68470,14 @@ var init_agent_companies_exporter = __esm({
68267
68470
  });
68268
68471
 
68269
68472
  // ../core/src/chat-store.ts
68270
- import { EventEmitter as EventEmitter17 } from "node:events";
68473
+ import { EventEmitter as EventEmitter18 } from "node:events";
68271
68474
  import { randomUUID as randomUUID19 } from "node:crypto";
68272
68475
  var ChatStore;
68273
68476
  var init_chat_store = __esm({
68274
68477
  "../core/src/chat-store.ts"() {
68275
68478
  "use strict";
68276
68479
  init_db();
68277
- ChatStore = class extends EventEmitter17 {
68480
+ ChatStore = class extends EventEmitter18 {
68278
68481
  constructor(fusionDir, db) {
68279
68482
  super();
68280
68483
  this.fusionDir = fusionDir;
@@ -69366,6 +69569,7 @@ var init_secrets_sync = __esm({
69366
69569
  // ../core/src/index.ts
69367
69570
  var src_exports = {};
69368
69571
  __export(src_exports, {
69572
+ ACTIVE_GOAL_LIMIT: () => ACTIVE_GOAL_LIMIT,
69369
69573
  AGENT_PERMISSIONS: () => AGENT_PERMISSIONS,
69370
69574
  AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: () => AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
69371
69575
  AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES,
@@ -69379,6 +69583,7 @@ __export(src_exports, {
69379
69583
  AUTOPILOT_STATES: () => AUTOPILOT_STATES,
69380
69584
  AUTO_RECOVERY_MODES: () => AUTO_RECOVERY_MODES,
69381
69585
  AUTO_SUMMARIZE_SCHEDULE_NAME: () => AUTO_SUMMARIZE_SCHEDULE_NAME,
69586
+ ActiveGoalLimitExceededError: () => ActiveGoalLimitExceededError,
69382
69587
  AgentCompaniesParseError: () => AgentCompaniesParseError,
69383
69588
  AgentStore: () => AgentStore,
69384
69589
  AiServiceError: () => AiServiceError,
@@ -69472,6 +69677,7 @@ __export(src_exports, {
69472
69677
  FirstRunDetector: () => FirstRunDetector,
69473
69678
  GLOBAL_SETTINGS_KEYS: () => GLOBAL_SETTINGS_KEYS,
69474
69679
  GlobalSettingsStore: () => GlobalSettingsStore,
69680
+ GoalStore: () => GoalStore,
69475
69681
  HIGH_FANOUT_BLOCKER_TODO_THRESHOLD: () => HIGH_FANOUT_BLOCKER_TODO_THRESHOLD,
69476
69682
  HandoffInvariantViolationError: () => HandoffInvariantViolationError,
69477
69683
  INSIGHT_EXTRACTION_SCHEDULE_NAME: () => INSIGHT_EXTRACTION_SCHEDULE_NAME,
@@ -69506,6 +69712,7 @@ __export(src_exports, {
69506
69712
  MEMORY_LONG_TERM_FILENAME: () => MEMORY_LONG_TERM_FILENAME,
69507
69713
  MEMORY_WORKING_PATH: () => MEMORY_WORKING_PATH,
69508
69714
  MEMORY_WORKSPACE_PATH: () => MEMORY_WORKSPACE_PATH,
69715
+ MERGER_MODES: () => MERGER_MODES,
69509
69716
  MERGE_ADVANCE_AUTO_SYNC_MODES: () => MERGE_ADVANCE_AUTO_SYNC_MODES,
69510
69717
  MERGE_AUDIT_AUTO_RECOVERY_MODES: () => MERGE_AUDIT_AUTO_RECOVERY_MODES,
69511
69718
  MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT: () => MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT,
@@ -69853,6 +70060,7 @@ __export(src_exports, {
69853
70060
  normalizeMergeConflictStrategy: () => normalizeMergeConflictStrategy,
69854
70061
  normalizeMergeIntegrationWorktreeMode: () => normalizeMergeIntegrationWorktreeMode,
69855
70062
  normalizeMergeStrategyOverlapBehavior: () => normalizeMergeStrategyOverlapBehavior,
70063
+ normalizeMergerMode: () => normalizeMergerMode,
69856
70064
  normalizeMessageParticipant: () => normalizeMessageParticipant,
69857
70065
  normalizePermissions: () => normalizePermissions,
69858
70066
  normalizePluginUiContributionDefinition: () => normalizePluginUiContributionDefinition,
@@ -70110,6 +70318,8 @@ var init_src = __esm({
70110
70318
  init_task_priority();
70111
70319
  init_mission_types();
70112
70320
  init_mission_store();
70321
+ init_goal_types();
70322
+ init_goal_store();
70113
70323
  init_central_core();
70114
70324
  init_central_db();
70115
70325
  init_node_connection();
@@ -70623,6 +70833,8 @@ var init_gating_classifications = __esm({
70623
70833
  "fn_insight_show",
70624
70834
  "fn_insight_run_list",
70625
70835
  "fn_insight_run_show",
70836
+ "fn_goal_list",
70837
+ "fn_goal_show",
70626
70838
  "fn_mission_list",
70627
70839
  "fn_mission_show",
70628
70840
  "fn_list_agents",
@@ -70740,15 +70952,15 @@ function evaluateAgentActionGate(params) {
70740
70952
  let resourceId;
70741
70953
  if (params.toolName === "bash") {
70742
70954
  const command = extractShellCommand(args);
70743
- const git2 = classifyGitCommand(command);
70744
- if (git2?.write) {
70955
+ const git3 = classifyGitCommand(command);
70956
+ if (git3?.write) {
70745
70957
  category = "git_write";
70746
- operation = git2.operation;
70958
+ operation = git3.operation;
70747
70959
  resourceType = "git";
70748
70960
  } else {
70749
70961
  category = "command_execution";
70750
- operation = git2?.operation ?? "shell command";
70751
- resourceType = git2 ? "git" : "command";
70962
+ operation = git3?.operation ?? "shell command";
70963
+ resourceType = git3 ? "git" : "command";
70752
70964
  }
70753
70965
  } else if (params.toolName === "write" || params.toolName === "edit") {
70754
70966
  category = "file_write_delete";
@@ -71988,8 +72200,8 @@ var init_context_limit_detector = __esm({
71988
72200
  import { existsSync as existsSync24, readFileSync as readFileSync8 } from "node:fs";
71989
72201
  import { homedir as homedir5 } from "node:os";
71990
72202
  import { join as join29 } from "node:path";
71991
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
71992
- import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
72203
+ import { AuthStorage } from "@earendil-works/pi-coding-agent";
72204
+ import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
71993
72205
  function getHomeDir5() {
71994
72206
  return process.env.HOME || process.env.USERPROFILE || homedir5();
71995
72207
  }
@@ -72398,7 +72610,7 @@ import {
72398
72610
  ModelRegistry,
72399
72611
  SessionManager,
72400
72612
  SettingsManager
72401
- } from "@mariozechner/pi-coding-agent";
72613
+ } from "@earendil-works/pi-coding-agent";
72402
72614
  function normalizeRtkRewriteOptions(options) {
72403
72615
  const modeEnv = process.env.FUSION_RTK_REWRITE?.toLowerCase();
72404
72616
  const envMode = modeEnv === "1" || modeEnv === "true" || modeEnv === "rewrite" ? "rewrite" : "off";
@@ -74928,9 +75140,9 @@ var init_web_search_provider = __esm({
74928
75140
  if (!isRetryableHttp || attempt >= RETRY_MAX_ATTEMPTS) {
74929
75141
  break;
74930
75142
  }
74931
- const delay4 = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
74932
- const jitter = delay4 * (Math.random() * 0.2 - 0.1);
74933
- const totalDelay = Math.max(0, delay4 + jitter);
75143
+ const delay5 = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
75144
+ const jitter = delay5 * (Math.random() * 0.2 - 0.1);
75145
+ const totalDelay = Math.max(0, delay5 + jitter);
74934
75146
  log9.warn(`retrying ${backend} search`, { attempt, totalDelay });
74935
75147
  await sleep2(totalDelay, signal);
74936
75148
  }
@@ -75384,7 +75596,7 @@ import { appendFile as appendFile3, mkdir as mkdir12, readFile as readFile14, re
75384
75596
  import { existsSync as existsSync26 } from "node:fs";
75385
75597
  import { createHash as createHash8 } from "node:crypto";
75386
75598
  import { join as join33, relative as relative6, resolve as resolve17 } from "node:path";
75387
- import { Type } from "@mariozechner/pi-ai";
75599
+ import { Type } from "@earendil-works/pi-ai";
75388
75600
  function sanitizeAgentMemoryId(agentId) {
75389
75601
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
75390
75602
  }
@@ -75554,11 +75766,11 @@ async function refreshAgentMemoryQmdIndex(rootDir, agentMemory) {
75554
75766
  return;
75555
75767
  }
75556
75768
  const promise = (async () => {
75557
- const { execFile: execFile19 } = await import("node:child_process");
75558
- const { promisify: promisify50 } = await import("node:util");
75559
- const execFileAsync14 = promisify50(execFile19);
75769
+ const { execFile: execFile20 } = await import("node:child_process");
75770
+ const { promisify: promisify51 } = await import("node:util");
75771
+ const execFileAsync15 = promisify51(execFile20);
75560
75772
  try {
75561
- await execFileAsync14("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
75773
+ await execFileAsync15("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
75562
75774
  cwd: rootDir,
75563
75775
  timeout: 4e3,
75564
75776
  maxBuffer: 512 * 1024
@@ -75571,8 +75783,8 @@ ${stderr}`)) {
75571
75783
  throw error;
75572
75784
  }
75573
75785
  }
75574
- await execFileAsync14("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
75575
- await execFileAsync14("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
75786
+ await execFileAsync15("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
75787
+ await execFileAsync15("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
75576
75788
  })();
75577
75789
  agentQmdRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
75578
75790
  try {
@@ -75627,10 +75839,10 @@ async function searchAgentMemoryWithQmd(rootDir, agentMemory, query, limit) {
75627
75839
  }
75628
75840
  try {
75629
75841
  await refreshAgentMemoryQmdIndex(rootDir, agentMemory);
75630
- const { execFile: execFile19 } = await import("node:child_process");
75631
- const { promisify: promisify50 } = await import("node:util");
75632
- const execFileAsync14 = promisify50(execFile19);
75633
- const { stdout } = await execFileAsync14("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
75842
+ const { execFile: execFile20 } = await import("node:child_process");
75843
+ const { promisify: promisify51 } = await import("node:util");
75844
+ const execFileAsync15 = promisify51(execFile20);
75845
+ const { stdout } = await execFileAsync15("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
75634
75846
  cwd: rootDir,
75635
75847
  timeout: 4e3,
75636
75848
  maxBuffer: 1024 * 1024
@@ -79134,6 +79346,24 @@ var init_webhook_provider = __esm({
79134
79346
  }
79135
79347
  });
79136
79348
 
79349
+ // ../engine/src/transient-merge-error-classifier.ts
79350
+ function classifyTransientMergeError(error) {
79351
+ if (!error) return null;
79352
+ if (/lease-handoff-failed[^a-z]+target-not-queued/i.test(error)) {
79353
+ return "lease-handoff-target-not-queued";
79354
+ }
79355
+ const sameSha = error.match(/advanced concurrently \(expected ([0-9a-f]{7,40}),\s+observed ([0-9a-f]{7,40})\)/i);
79356
+ if (sameSha && sameSha[1].toLowerCase() === sameSha[2].toLowerCase()) {
79357
+ return "spurious-concurrent-advance-same-sha";
79358
+ }
79359
+ return null;
79360
+ }
79361
+ var init_transient_merge_error_classifier = __esm({
79362
+ "../engine/src/transient-merge-error-classifier.ts"() {
79363
+ "use strict";
79364
+ }
79365
+ });
79366
+
79137
79367
  // ../engine/src/notification/notification-service.ts
79138
79368
  var NotificationService;
79139
79369
  var init_notification_service = __esm({
@@ -79142,6 +79372,7 @@ var init_notification_service = __esm({
79142
79372
  init_src();
79143
79373
  init_notifier();
79144
79374
  init_logger2();
79375
+ init_transient_merge_error_classifier();
79145
79376
  init_ntfy_provider();
79146
79377
  init_webhook_provider();
79147
79378
  NotificationService = class {
@@ -79243,6 +79474,14 @@ var init_notification_service = __esm({
79243
79474
  return;
79244
79475
  }
79245
79476
  if (task.status === "failed") {
79477
+ const transientClass = classifyTransientMergeError(task.error);
79478
+ if (transientClass) {
79479
+ this.failureNotificationSuppressedCount += 1;
79480
+ schedulerLog.log(
79481
+ `[notify] ${task.id} transient merge failure (${transientClass}) \u2014 suppressed notification (self-heal in flight)`
79482
+ );
79483
+ return;
79484
+ }
79246
79485
  if (this.failureNotificationMode === "all") {
79247
79486
  this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
79248
79487
  } else {
@@ -79559,6 +79798,14 @@ var init_notification_service = __esm({
79559
79798
  schedulerLog.log(`[notify] ${taskId} no longer failed at dispatch time \u2014 suppressed notification`);
79560
79799
  return;
79561
79800
  }
79801
+ const transientClassAtDispatch = classifyTransientMergeError(task.error);
79802
+ if (transientClassAtDispatch) {
79803
+ this.failureNotificationSuppressedCount += 1;
79804
+ schedulerLog.log(
79805
+ `[notify] ${taskId} transient merge failure (${transientClassAtDispatch}) at dispatch time \u2014 suppressed notification (self-heal in flight)`
79806
+ );
79807
+ return;
79808
+ }
79562
79809
  const isTerminal = task.paused === true || task.column === "in-review";
79563
79810
  if (this.failureNotificationMode === "terminal-only" && !isTerminal) {
79564
79811
  this.failureNotificationSuppressedCount += 1;
@@ -79811,6 +80058,19 @@ function truncateNtfyMessage(message) {
79811
80058
  }
79812
80059
  return best;
79813
80060
  }
80061
+ function ntfyPriorityToInt(priority) {
80062
+ switch (priority) {
80063
+ case "low":
80064
+ return 2;
80065
+ case "high":
80066
+ return 4;
80067
+ case "urgent":
80068
+ return 5;
80069
+ case "default":
80070
+ default:
80071
+ return 3;
80072
+ }
80073
+ }
79814
80074
  function resolveNtfyEvents(events) {
79815
80075
  return events ? [...events] : [...DEFAULT_NTFY_EVENTS];
79816
80076
  }
@@ -79860,10 +80120,10 @@ async function sendNtfyNotificationWithResult({
79860
80120
  const truncatedMessage = truncateNtfyMessage(message);
79861
80121
  const latin1Safe = isLatin1Safe(truncatedTitle) && isLatin1Safe(truncatedMessage);
79862
80122
  const headers = {
79863
- Priority: priority,
79864
80123
  "Content-Type": latin1Safe ? "text/plain" : "application/json"
79865
80124
  };
79866
80125
  if (latin1Safe) {
80126
+ headers.Priority = priority;
79867
80127
  headers.Title = truncatedTitle;
79868
80128
  if (clickUrl) {
79869
80129
  headers.Click = clickUrl;
@@ -79879,7 +80139,7 @@ async function sendNtfyNotificationWithResult({
79879
80139
  topic,
79880
80140
  title: truncatedTitle,
79881
80141
  message: truncatedMessage,
79882
- priority,
80142
+ priority: ntfyPriorityToInt(priority),
79883
80143
  ...clickUrl ? { click: clickUrl } : {}
79884
80144
  }),
79885
80145
  signal
@@ -85151,11 +85411,11 @@ var init_step_session_executor = __esm({
85151
85411
  }
85152
85412
  if (attempt > 0) {
85153
85413
  retries++;
85154
- const delay4 = RETRY_DELAYS_MS[attempt - 1] ?? 15e3;
85414
+ const delay5 = RETRY_DELAYS_MS[attempt - 1] ?? 15e3;
85155
85415
  stepExecLog.log(
85156
- `Step ${stepIndex} retry ${attempt}/${MAX_STEP_RETRIES} (delay=${delay4}ms) for task ${taskDetail.id}`
85416
+ `Step ${stepIndex} retry ${attempt}/${MAX_STEP_RETRIES} (delay=${delay5}ms) for task ${taskDetail.id}`
85157
85417
  );
85158
- await sleep3(delay4);
85418
+ await sleep3(delay5);
85159
85419
  }
85160
85420
  const agentLogger = new AgentLogger({
85161
85421
  store: this.store,
@@ -85921,9 +86181,9 @@ async function withRateLimitRetry(fn2, options = {}) {
85921
86181
  }
85922
86182
  const rawDelay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
85923
86183
  const jitter = rawDelay * 0.1 * (2 * Math.random() - 1);
85924
- const delay4 = Math.max(0, Math.round(rawDelay + jitter));
85925
- onRetry?.(attempt + 1, delay4, error);
85926
- await sleep4(delay4, signal);
86184
+ const delay5 = Math.max(0, Math.round(rawDelay + jitter));
86185
+ onRetry?.(attempt + 1, delay5, error);
86186
+ await sleep4(delay5, signal);
85927
86187
  }
85928
86188
  }
85929
86189
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
@@ -88993,10 +89253,13 @@ function decideAutoPrerebase(input) {
88993
89253
  if (hotMatches.length > 0) {
88994
89254
  return { fire: true, reason: "hot-file", commitsBehind, hotMatches };
88995
89255
  }
88996
- const threshold = input.settings.prerebaseDivergenceThreshold ?? 0;
88997
- if (threshold > 0 && commitsBehind > threshold) {
89256
+ const threshold = input.settings.prerebaseDivergenceThreshold ?? 1;
89257
+ if (threshold > 0 && commitsBehind >= threshold) {
88998
89258
  return { fire: true, reason: "divergence-threshold", commitsBehind, hotMatches: [] };
88999
89259
  }
89260
+ if (commitsBehind > 0) {
89261
+ return { fire: true, reason: "safety-fallback-any-divergence", commitsBehind, hotMatches: [] };
89262
+ }
89000
89263
  return { fire: false, reason: "no-divergence", commitsBehind, hotMatches: [] };
89001
89264
  }
89002
89265
  async function runAutoPrerebase(deps) {
@@ -90914,12 +91177,22 @@ async function advanceIntegrationBranchRef(args) {
90914
91177
  return { advanced: true, previousSha: expectedCurrentSha, newSha };
90915
91178
  } catch (error) {
90916
91179
  const diagnostic = error instanceof Error ? error.message : String(error);
90917
- const lower = diagnostic.toLowerCase();
90918
- const isConcurrent = lower.includes("cannot lock ref") || lower.includes("is at") || lower.includes("expected");
90919
- const reason = isConcurrent ? "concurrent-advance" : "ref-update-refused";
91180
+ let postFailureCurrentSha;
91181
+ try {
91182
+ const { stdout: postStdout } = await testHooks.runGit(
91183
+ ["rev-parse", "--verify", ref],
91184
+ rootDir
91185
+ );
91186
+ postFailureCurrentSha = postStdout.trim() || void 0;
91187
+ } catch {
91188
+ postFailureCurrentSha = observedCurrentSha;
91189
+ }
91190
+ const effectiveObserved = postFailureCurrentSha || observedCurrentSha;
91191
+ const refMoved = !!effectiveObserved && effectiveObserved !== expectedCurrentSha;
91192
+ const reason = refMoved ? "concurrent-advance" : "ref-update-refused";
90920
91193
  await emitRefAdvance({
90921
91194
  succeeded: false,
90922
- fromSha: observedCurrentSha || expectedCurrentSha,
91195
+ fromSha: effectiveObserved || expectedCurrentSha,
90923
91196
  toSha: newSha,
90924
91197
  error: `${reason}: ${diagnostic}`
90925
91198
  });
@@ -90927,7 +91200,7 @@ async function advanceIntegrationBranchRef(args) {
90927
91200
  advanced: false,
90928
91201
  reason,
90929
91202
  diagnostic,
90930
- observedCurrentSha
91203
+ observedCurrentSha: effectiveObserved
90931
91204
  };
90932
91205
  }
90933
91206
  }
@@ -91404,6 +91677,7 @@ __export(merger_exports, {
91404
91677
  VERIFICATION_LOG_MAX_CHARS: () => VERIFICATION_LOG_MAX_CHARS,
91405
91678
  VerificationError: () => VerificationError,
91406
91679
  __executePostMergeScriptStepForTests: () => __executePostMergeScriptStepForTests,
91680
+ __testOnlyBuildDeterministicMergeMessage: () => buildDeterministicMergeMessage,
91407
91681
  __test__: () => __test__,
91408
91682
  aiMergeTask: () => aiMergeTask,
91409
91683
  applyAutostashBySha: () => applyAutostashBySha,
@@ -91417,6 +91691,7 @@ __export(merger_exports, {
91417
91691
  classifyConflict: () => classifyConflict,
91418
91692
  classifyOwnedLandedEvidence: () => classifyOwnedLandedEvidence,
91419
91693
  commitOrAmendMergeWithFixes: () => commitOrAmendMergeWithFixes,
91694
+ composeMergeCommitBody: () => composeMergeCommitBody,
91420
91695
  deriveDeterministicSubjectSummary: () => deriveDeterministicSubjectSummary,
91421
91696
  deriveScopedPnpmTestCommand: () => deriveScopedPnpmTestCommand,
91422
91697
  detectResolvableConflicts: () => detectResolvableConflicts,
@@ -93906,6 +94181,23 @@ async function generateAiMergeSummary(commitLog, diffStat, settings, rootDir) {
93906
94181
  return null;
93907
94182
  }
93908
94183
  }
94184
+ async function generateAiMergeBody(commitLog, diffStat, settings, rootDir, branch, taskId, signal) {
94185
+ const cleanStat = diffStat.trim();
94186
+ if (!cleanStat) return null;
94187
+ try {
94188
+ const resolved = resolveTitleSummarizerSettingsModel(settings);
94189
+ return await summarizeCommitBody(cleanStat, rootDir, resolved.provider, resolved.modelId, {
94190
+ branch,
94191
+ taskId,
94192
+ commitLog,
94193
+ signal
94194
+ });
94195
+ } catch (err) {
94196
+ const message = err instanceof Error ? err.message : String(err);
94197
+ mergerLog.warn(`AI merge body failed; using deterministic fallback (${message})`);
94198
+ return null;
94199
+ }
94200
+ }
93909
94201
  async function generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, signal) {
93910
94202
  try {
93911
94203
  const resolved = resolveTitleSummarizerSettingsModel(settings);
@@ -93973,22 +94265,43 @@ function deriveDeterministicSubjectSummary(commitLog) {
93973
94265
  const summary = extras > 0 ? `${headline} (+${extras} more)` : headline;
93974
94266
  return summary;
93975
94267
  }
93976
- async function buildDeterministicMergeMessage(params) {
93977
- const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary, aiSubject } = params;
93978
- const prefix = includeTaskId ? `feat(${taskId})` : "feat";
93979
- const trimmedAiSubject = aiSubject?.trim() ?? "";
93980
- const derived = trimmedAiSubject.length === 0 ? deriveDeterministicSubjectSummary(commitLog ?? "") : null;
93981
- const subjectSummary = trimmedAiSubject.length > 0 ? trimmedAiSubject : derived ?? `merge ${branch}`;
93982
- const subject = `${prefix}: ${subjectSummary}`;
94268
+ function composeMergeCommitBody(params) {
94269
+ const { branch, commitLog, diffStat, aiSummary, aiBody } = params;
94270
+ const trimmedSummary = aiSummary?.trim() ?? "";
94271
+ const trimmedAiBody = aiBody?.trim() ?? "";
93983
94272
  const trimmedCommitLog = commitLog?.trim() ?? "";
93984
94273
  const trimmedDiffStat = diffStat?.trim() ?? "";
93985
94274
  const commitsSection = trimmedCommitLog.length > 0 ? trimmedCommitLog : `- merge ${branch}`;
93986
- const body = aiSummary?.trim().length ? aiSummary.trim() : [
94275
+ const parts = [];
94276
+ if (trimmedSummary.length > 0) parts.push(trimmedSummary);
94277
+ if (trimmedAiBody.length > 0) parts.push(trimmedAiBody);
94278
+ const deterministicFallback = [
93987
94279
  `Commits merged:
93988
94280
  ${commitsSection}`,
93989
94281
  trimmedDiffStat.length > 0 ? `Files changed:
93990
94282
  ${trimmedDiffStat}` : ""
93991
94283
  ].filter(Boolean).join("\n\n");
94284
+ if (parts.length === 0) return deterministicFallback;
94285
+ if (trimmedDiffStat.length > 0) {
94286
+ parts.push(`Files changed:
94287
+ ${trimmedDiffStat}`);
94288
+ }
94289
+ return parts.join("\n\n");
94290
+ }
94291
+ async function buildDeterministicMergeMessage(params) {
94292
+ const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary, aiBody, aiSubject } = params;
94293
+ const prefix = includeTaskId ? `feat(${taskId})` : "feat";
94294
+ const trimmedAiSubject = aiSubject?.trim() ?? "";
94295
+ const derived = trimmedAiSubject.length === 0 ? deriveDeterministicSubjectSummary(commitLog ?? "") : null;
94296
+ const subjectSummary = trimmedAiSubject.length > 0 ? trimmedAiSubject : derived ?? `merge ${branch}`;
94297
+ const subject = `${prefix}: ${subjectSummary}`;
94298
+ const body = composeMergeCommitBody({
94299
+ branch,
94300
+ commitLog,
94301
+ diffStat,
94302
+ aiSummary,
94303
+ aiBody
94304
+ });
93992
94305
  const escape = (s3) => s3.replace(/(["\\$`])/g, "\\$1");
93993
94306
  return {
93994
94307
  subjectArg: `-m "${escape(subject)}"`,
@@ -94100,7 +94413,7 @@ async function filterStagedGitignoredPaths(rootDir, taskId) {
94100
94413
  return { unstaged: [], remainingStaged };
94101
94414
  }
94102
94415
  }
94103
- async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, includeTaskId, preAttemptHeadSha, authorArg, diffStat, settings, signal, aiSummary, aiSubject, fixModifiedFiles = /* @__PURE__ */ new Set(), store, auditor) {
94416
+ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, includeTaskId, preAttemptHeadSha, authorArg, diffStat, settings, signal, aiSummary, aiBody, aiSubject, fixModifiedFiles = /* @__PURE__ */ new Set(), store, auditor) {
94104
94417
  try {
94105
94418
  const { stdout: squashStagedOut } = await execAsync26("git diff --cached --name-only", {
94106
94419
  cwd: rootDir,
@@ -94400,6 +94713,7 @@ ${msg}`;
94400
94713
  diffStat: messageDiffStat,
94401
94714
  includeTaskId,
94402
94715
  aiSummary,
94716
+ aiBody,
94403
94717
  aiSubject
94404
94718
  });
94405
94719
  let lineageId;
@@ -97593,8 +97907,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97593
97907
  mergerLog.log(`${taskId}: ${routeMessage}`);
97594
97908
  await store.appendAgentLog(taskId, routeMessage, "text", void 0, "merger");
97595
97909
  }
97596
- const aiMergeSummary = settings.useAiMergeCommitSummary ? await generateAiMergeSummary(commitLog, diffStat, settings, rootDir) : null;
97597
- const aiMergeSubject = settings.useAiMergeCommitSummary ? await generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal) : null;
97910
+ const [aiMergeSummary, aiMergeBody, aiMergeSubject] = settings.useAiMergeCommitSummary ? await Promise.all([
97911
+ generateAiMergeSummary(commitLog, diffStat, settings, rootDir),
97912
+ generateAiMergeBody(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal),
97913
+ generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal)
97914
+ ]) : [null, null, null];
97598
97915
  try {
97599
97916
  const scopeResult = await validateDiffScope(store, taskId, diffStat, settings.strictScopeEnforcement);
97600
97917
  for (const warning of scopeResult.warnings) {
@@ -97670,6 +97987,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97670
97987
  commitLog,
97671
97988
  diffStat,
97672
97989
  aiSummary: aiMergeSummary,
97990
+ aiBody: aiMergeBody,
97673
97991
  aiSubject: aiMergeSubject,
97674
97992
  includeTaskId,
97675
97993
  sourceIssueRef,
@@ -97827,6 +98145,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97827
98145
  settings,
97828
98146
  options.signal,
97829
98147
  aiMergeSummary,
98148
+ aiMergeBody,
97830
98149
  aiMergeSubject,
97831
98150
  verificationFixModifiedFiles,
97832
98151
  store,
@@ -97958,6 +98277,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97958
98277
  settings,
97959
98278
  options.signal,
97960
98279
  aiMergeSummary,
98280
+ aiMergeBody,
97961
98281
  aiMergeSubject,
97962
98282
  buildFixModifiedFiles,
97963
98283
  store,
@@ -98268,6 +98588,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98268
98588
  const recordedFilesChanged = mergeWasEmpty ? 0 : filesChanged;
98269
98589
  const recordedInsertions = mergeWasEmpty ? 0 : insertions;
98270
98590
  const recordedDeletions = mergeWasEmpty ? 0 : deletions;
98591
+ const mergeConfirmedAtThisPoint = !reuseTaskWorktreeMerge;
98271
98592
  const mergeDetails = {
98272
98593
  commitSha: recordedSha,
98273
98594
  rebaseBaseSha: !mergeWasEmpty && rebaseMergeBaseSha ? rebaseMergeBaseSha : void 0,
@@ -98278,9 +98599,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98278
98599
  noOpVerifiedShortCircuit,
98279
98600
  landedFilesAttributionRestricted,
98280
98601
  landedFilesCaptureFallback,
98602
+ // Keep mergeDetails headline-only for dashboard cards; rich bullet body
98603
+ // is used in the actual git commit message composition path.
98281
98604
  mergeCommitMessage: aiMergeSummary || commitLog,
98282
98605
  mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
98283
- mergeConfirmed: true,
98606
+ mergeConfirmed: mergeConfirmedAtThisPoint,
98284
98607
  mergeTargetBranch: mergeTarget.branch,
98285
98608
  mergeTargetSource: mergeTarget.source,
98286
98609
  resolutionStrategy: result.resolutionStrategy,
@@ -98374,6 +98697,19 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98374
98697
  mergerLog.log(
98375
98698
  `${taskId}: ${integrationBranch2} advanced to ${worktreeHeadSha.slice(0, 8)} via update-ref; your checked-out worktree at ${projectRootDir} is now behind`
98376
98699
  );
98700
+ try {
98701
+ const currentTask = await store.getTask(taskId).catch(() => null);
98702
+ const currentMergeDetails = currentTask?.mergeDetails;
98703
+ if (currentMergeDetails && !currentMergeDetails.mergeConfirmed) {
98704
+ await store.updateTask(taskId, {
98705
+ mergeDetails: { ...currentMergeDetails, mergeConfirmed: true }
98706
+ });
98707
+ }
98708
+ } catch (promoteErr) {
98709
+ mergerLog.warn(
98710
+ `${taskId}: failed to promote mergeConfirmed post-ref-advance: ${promoteErr instanceof Error ? promoteErr.message : String(promoteErr)}`
98711
+ );
98712
+ }
98377
98713
  const autoSyncMode = normalizeMergeAdvanceAutoSyncMode(settings.mergeAdvanceAutoSync);
98378
98714
  if (autoSyncMode !== "off") {
98379
98715
  try {
@@ -98738,6 +99074,7 @@ async function executeMergeAttempt(params, aiTracker) {
98738
99074
  commitLog,
98739
99075
  diffStat,
98740
99076
  aiSummary,
99077
+ aiBody,
98741
99078
  aiSubject,
98742
99079
  includeTaskId,
98743
99080
  sourceIssueRef,
@@ -98855,6 +99192,7 @@ async function executeMergeAttempt(params, aiTracker) {
98855
99192
  diffStat,
98856
99193
  includeTaskId,
98857
99194
  aiSummary: safeBody,
99195
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
98858
99196
  aiSubject
98859
99197
  });
98860
99198
  await enforceSquashFileScopeInvariant({
@@ -98996,6 +99334,7 @@ async function executeMergeAttempt(params, aiTracker) {
98996
99334
  commitLog,
98997
99335
  diffStat,
98998
99336
  aiSummary,
99337
+ aiBody,
98999
99338
  aiSubject,
99000
99339
  includeTaskId,
99001
99340
  hasConflicts,
@@ -99048,6 +99387,7 @@ async function executeMergeAttempt(params, aiTracker) {
99048
99387
  diffStat: actualContext.diffStat || diffStat,
99049
99388
  includeTaskId,
99050
99389
  aiSummary,
99390
+ aiBody,
99051
99391
  aiSubject
99052
99392
  });
99053
99393
  const trailerArg = buildTaskTrailerArgs(taskId);
@@ -99142,7 +99482,7 @@ async function attemptWithMixedSideStrategy(params, strategy, aiTracker) {
99142
99482
  }
99143
99483
  }
99144
99484
  async function finalizeSideStrategyAttempt(params, side, aiTracker) {
99145
- const { rootDir, branch, commitLog, diffStat, aiSummary, aiSubject, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
99485
+ const { rootDir, branch, commitLog, diffStat, aiSummary, aiBody, aiSubject, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
99146
99486
  const staged = execSyncText("git diff --cached --quiet 2>&1; echo $?", {
99147
99487
  cwd: rootDir,
99148
99488
  encoding: "utf-8"
@@ -99183,7 +99523,8 @@ async function finalizeSideStrategyAttempt(params, side, aiTracker) {
99183
99523
  commitLog,
99184
99524
  diffStat,
99185
99525
  includeTaskId,
99186
- aiSummary: aiSummary?.trim().length ? aiSummary : safeBody,
99526
+ aiSummary,
99527
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
99187
99528
  aiSubject
99188
99529
  });
99189
99530
  await enforceSquashFileScopeInvariant({
@@ -99241,6 +99582,7 @@ async function runAiAgentForCommit(params) {
99241
99582
  commitLog,
99242
99583
  diffStat,
99243
99584
  aiSummary,
99585
+ aiBody,
99244
99586
  aiSubject,
99245
99587
  includeTaskId,
99246
99588
  hasConflicts,
@@ -99462,7 +99804,8 @@ async function runAiAgentForCommit(params) {
99462
99804
  commitLog,
99463
99805
  diffStat,
99464
99806
  includeTaskId,
99465
- aiSummary: aiSummary?.trim().length ? aiSummary : safeBody,
99807
+ aiSummary,
99808
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
99466
99809
  aiSubject
99467
99810
  });
99468
99811
  await runDiffVolumeGate({
@@ -100287,7 +100630,7 @@ function isNoTaskDoneFailure2(task) {
100287
100630
  function hasStepProgress2(task) {
100288
100631
  return task.steps.some((step) => step.status !== "pending");
100289
100632
  }
100290
- var log20, worktreeMetadataReconcileLog, execAsync27, yieldEventLoop2, DONE_TASK_INTEGRITY_SWEEP_LIMIT, BOARD_STALL_NOTIFICATION_COOLDOWN_MS, DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS, STALE_ACTIVE_BRANCH_EXECUTION_GRACE_MS, COMPLETION_HANDOFF_LIMBO_GRACE_MS, MAX_COMPLETION_HANDOFF_LIMBO_RECOVERIES, APPROVED_TRIAGE_RECOVERY_GRACE_MS, STARVED_REFINEMENT_RECOVERY_GRACE_MS, STARVED_PEER_PROGRESS_THRESHOLD, STARVED_REFINEMENT_ESCALATION_COOLDOWN_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES2, NON_TERMINAL_STEP_STATUSES2, STRANDED_COMPLETED_TODO_ACTIVE_STATUSES, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_WORKTREE_SESSION_RETRIES, MAX_AUTO_MERGE_RETRIES, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DEFAULT_STALE_MERGING_FANOUT_MIN_AGE_MS, DEFAULT_UNBACKED_MERGING_FANOUT_GRACE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, RUNNING_ON_INACTIVE_TASK_STALE_RUN_MS, SelfHealingManager;
100633
+ var log20, worktreeMetadataReconcileLog, execAsync27, yieldEventLoop2, DONE_TASK_INTEGRITY_SWEEP_LIMIT, BOARD_STALL_NOTIFICATION_COOLDOWN_MS, DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS, STALE_ACTIVE_BRANCH_EXECUTION_GRACE_MS, COMPLETION_HANDOFF_LIMBO_GRACE_MS, MAX_COMPLETION_HANDOFF_LIMBO_RECOVERIES, APPROVED_TRIAGE_RECOVERY_GRACE_MS, STARVED_REFINEMENT_RECOVERY_GRACE_MS, STARVED_PEER_PROGRESS_THRESHOLD, STARVED_REFINEMENT_ESCALATION_COOLDOWN_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES2, NON_TERMINAL_STEP_STATUSES2, STRANDED_COMPLETED_TODO_ACTIVE_STATUSES, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_WORKTREE_SESSION_RETRIES, MAX_AUTO_MERGE_RETRIES, MAX_TRANSIENT_MERGE_RECOVERIES, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DEFAULT_STALE_MERGING_FANOUT_MIN_AGE_MS, DEFAULT_UNBACKED_MERGING_FANOUT_GRACE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, RUNNING_ON_INACTIVE_TASK_STALE_RUN_MS, SelfHealingManager;
100291
100634
  var init_self_healing = __esm({
100292
100635
  "../engine/src/self-healing.ts"() {
100293
100636
  "use strict";
@@ -100308,6 +100651,8 @@ var init_self_healing = __esm({
100308
100651
  init_foreign_only_contamination();
100309
100652
  init_notifier();
100310
100653
  init_dependency_blocked_todo_reporter();
100654
+ init_transient_merge_error_classifier();
100655
+ init_transient_merge_error_classifier();
100311
100656
  log20 = createLogger2("self-healing");
100312
100657
  worktreeMetadataReconcileLog = createLogger2("worktree-metadata-reconcile");
100313
100658
  execAsync27 = promisify35(exec29);
@@ -100346,6 +100691,7 @@ var init_self_healing = __esm({
100346
100691
  MAX_TASK_DONE_RETRIES = 3;
100347
100692
  MAX_WORKTREE_SESSION_RETRIES = 3;
100348
100693
  MAX_AUTO_MERGE_RETRIES = 3;
100694
+ MAX_TRANSIENT_MERGE_RECOVERIES = 2;
100349
100695
  MAX_STARVATION_DROPS = 3;
100350
100696
  DEADLOCK_RECOVERY_COOLDOWN_MS = 15 * 6e4;
100351
100697
  DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS = 5 * 6e4;
@@ -100543,6 +100889,7 @@ var init_self_healing = __esm({
100543
100889
  { name: "stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks().then(() => void 0) },
100544
100890
  { name: "failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps().then(() => void 0) },
100545
100891
  { name: "interrupted-merging", fn: () => this.recoverInterruptedMergingTasks().then(() => void 0) },
100892
+ { name: "transient-merge-failures", fn: () => this.recoverTransientMergeFailures().then(() => void 0) },
100546
100893
  { name: "done-merge-metadata", fn: () => this.recoverDoneTaskMergeMetadata().then(() => void 0) },
100547
100894
  { name: "reconcile-done-task-integrity", fn: () => this.reconcileDoneTaskIntegrity().then(() => void 0) },
100548
100895
  // FN-5092: must run BEFORE any merger pickup path so the merger queue is
@@ -100637,8 +100984,8 @@ var init_self_healing = __esm({
100637
100984
  this.lastPauseTriggeredAt = Date.now();
100638
100985
  const baseDelay = settings.autoUnpauseBaseDelayMs ?? 3e5;
100639
100986
  const maxDelay = settings.autoUnpauseMaxDelayMs ?? 36e5;
100640
- const delay4 = Math.min(baseDelay * Math.pow(2, this.unpauseAttempt), maxDelay);
100641
- this.scheduleUnpause(delay4);
100987
+ const delay5 = Math.min(baseDelay * Math.pow(2, this.unpauseAttempt), maxDelay);
100988
+ this.scheduleUnpause(delay5);
100642
100989
  }
100643
100990
  if (previous.globalPause && !settings.globalPause) {
100644
100991
  this.cancelUnpauseTimer();
@@ -101072,6 +101419,7 @@ var init_self_healing = __esm({
101072
101419
  { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
101073
101420
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
101074
101421
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
101422
+ { name: "recover-transient-merge-failures", fn: () => this.recoverTransientMergeFailures() },
101075
101423
  { name: "recover-done-merge-metadata", fn: () => this.recoverDoneTaskMergeMetadata() },
101076
101424
  { name: "recover-stale-merging-status", fn: () => this.recoverStaleMergingStatus() },
101077
101425
  { name: "finalize-noop-review", fn: () => this.finalizeNoOpReviewTasks() },
@@ -104244,6 +104592,142 @@ var init_self_healing = __esm({
104244
104592
  * No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
104245
104593
  * @returns Number of tasks finalized or unblocked
104246
104594
  */
104595
+ /**
104596
+ * FN-5627 follow-up: recover in-review tasks that are stuck with
104597
+ * `mergeRetries >= MAX_AUTO_MERGE_RETRIES` and `status='failed'` due to a
104598
+ * TRANSIENT merge failure class (e.g., `target-not-queued` lease handoff
104599
+ * race, legacy same-SHA spurious concurrent-advance). These tasks are
104600
+ * NOT really stuck — they just hit a race or a misclassified ref-update
104601
+ * failure that would have cleared on a fresh attempt. Without this sweep,
104602
+ * the only path forward is manual intervention (the
104603
+ * `AUTO_MERGE_COOLDOWN_MS`-based reset takes hours).
104604
+ *
104605
+ * For each matching task:
104606
+ * - Reset `mergeRetries=0`, clear `status` and `error`.
104607
+ * - Increment `mergeDetails.transientRecoveryCount`.
104608
+ * - Re-enqueue via `requeueForAutoMerge`.
104609
+ * - Bounded by `MAX_TRANSIENT_MERGE_RECOVERIES`; exhausted tasks stay
104610
+ * parked as failed and emit `merger:transient-failure-budget-exhausted`
104611
+ * once for diagnostic visibility.
104612
+ *
104613
+ * No-op when `settings.autoMerge === false`, no `requeueForAutoMerge`
104614
+ * callback is wired, or global/engine pause is active.
104615
+ *
104616
+ * @returns Number of tasks recovered
104617
+ */
104618
+ async recoverTransientMergeFailures() {
104619
+ const requeue = this.options.requeueForAutoMerge;
104620
+ if (!requeue) return 0;
104621
+ try {
104622
+ const settings = await this.store.getSettings();
104623
+ if (settings.autoMerge === false) return 0;
104624
+ if (settings.globalPause || settings.enginePaused) return 0;
104625
+ const slim = await this.store.listTasks({ column: "in-review", slim: true });
104626
+ const candidates = slim.filter(
104627
+ (t) => t.column === "in-review" && t.status === "failed" && (t.mergeRetries ?? 0) >= MAX_AUTO_MERGE_RETRIES && typeof t.error === "string" && t.error.length > 0 && classifyTransientMergeError(t.error) !== null
104628
+ );
104629
+ if (candidates.length === 0) return 0;
104630
+ log20.warn(
104631
+ `Found ${candidates.length} in-review task(s) with transient merge failures stuck at mergeRetries=${MAX_AUTO_MERGE_RETRIES}; attempting auto-recovery`
104632
+ );
104633
+ let recovered = 0;
104634
+ for (const slimTask of candidates) {
104635
+ const task = await this.store.getTask(slimTask.id).catch(() => null);
104636
+ if (!task) continue;
104637
+ if (task.column !== "in-review" || task.status !== "failed" || (task.mergeRetries ?? 0) < MAX_AUTO_MERGE_RETRIES) {
104638
+ continue;
104639
+ }
104640
+ const errorText = task.error ?? "";
104641
+ const transientClass = classifyTransientMergeError(errorText);
104642
+ if (!transientClass) continue;
104643
+ const currentCount = task.mergeDetails?.transientRecoveryCount ?? 0;
104644
+ const errorSnippet = errorText.slice(0, 200);
104645
+ const audit = createRunAuditor(this.store, {
104646
+ runId: generateSyntheticRunId("self-heal-transient-merge-recovery", task.id),
104647
+ agentId: "self-healing",
104648
+ taskId: task.id,
104649
+ phase: "recover-transient-merge-failures"
104650
+ });
104651
+ if (currentCount >= MAX_TRANSIENT_MERGE_RECOVERIES) {
104652
+ if (!errorText.includes("[transient-recovery-budget-exhausted]")) {
104653
+ await this.store.logEntry(
104654
+ task.id,
104655
+ `[FN-5627] Transient merge failure auto-recovery budget exhausted (${transientClass}, ${currentCount}/${MAX_TRANSIENT_MERGE_RECOVERIES}). Task remains parked in in-review for manual review.`
104656
+ );
104657
+ await this.store.updateTask(task.id, {
104658
+ error: `${errorText} [transient-recovery-budget-exhausted]`
104659
+ });
104660
+ try {
104661
+ await audit.database({
104662
+ type: "merger:transient-failure-budget-exhausted",
104663
+ target: task.id,
104664
+ metadata: {
104665
+ taskId: task.id,
104666
+ transientClass,
104667
+ recoveryCount: currentCount,
104668
+ maxRecoveries: MAX_TRANSIENT_MERGE_RECOVERIES,
104669
+ errorSnippet
104670
+ }
104671
+ });
104672
+ } catch (auditErr) {
104673
+ log20.warn(
104674
+ `recoverTransientMergeFailures: audit emit failed for ${task.id}: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
104675
+ );
104676
+ }
104677
+ }
104678
+ continue;
104679
+ }
104680
+ const nextCount = currentCount + 1;
104681
+ await this.store.logEntry(
104682
+ task.id,
104683
+ `Auto-recovered: transient merge failure (${transientClass}); resetting mergeRetries=0 and re-enqueueing (recovery ${nextCount}/${MAX_TRANSIENT_MERGE_RECOVERIES}) [FN-5627]`
104684
+ );
104685
+ await this.store.updateTask(task.id, {
104686
+ mergeRetries: 0,
104687
+ status: null,
104688
+ error: null,
104689
+ mergeDetails: {
104690
+ ...task.mergeDetails,
104691
+ transientRecoveryCount: nextCount
104692
+ }
104693
+ });
104694
+ try {
104695
+ await audit.database({
104696
+ type: "merger:transient-failure-auto-recovered",
104697
+ target: task.id,
104698
+ metadata: {
104699
+ taskId: task.id,
104700
+ transientClass,
104701
+ mergeRetries: task.mergeRetries ?? 0,
104702
+ recoveryCount: nextCount,
104703
+ errorSnippet
104704
+ }
104705
+ });
104706
+ } catch (auditErr) {
104707
+ log20.warn(
104708
+ `recoverTransientMergeFailures: audit emit failed for ${task.id}: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
104709
+ );
104710
+ }
104711
+ try {
104712
+ await requeue(task.id);
104713
+ recovered++;
104714
+ } catch (requeueErr) {
104715
+ log20.warn(
104716
+ `recoverTransientMergeFailures: requeue failed for ${task.id}: ${requeueErr instanceof Error ? requeueErr.message : String(requeueErr)}`
104717
+ );
104718
+ }
104719
+ }
104720
+ if (recovered > 0) {
104721
+ log20.log(`recoverTransientMergeFailures: re-enqueued ${recovered} stuck in-review task(s)`);
104722
+ }
104723
+ return recovered;
104724
+ } catch (err) {
104725
+ log20.warn(
104726
+ `recoverTransientMergeFailures sweep failed: ${err instanceof Error ? err.message : String(err)}`
104727
+ );
104728
+ return 0;
104729
+ }
104730
+ }
104247
104731
  async recoverInterruptedMergingTasks() {
104248
104732
  try {
104249
104733
  const settings = await this.store.getSettings();
@@ -106545,7 +107029,7 @@ var init_self_healing = __esm({
106545
107029
  });
106546
107030
 
106547
107031
  // ../engine/src/triage.ts
106548
- import { Type as Type3 } from "@mariozechner/pi-ai";
107032
+ import { Type as Type3 } from "@earendil-works/pi-ai";
106549
107033
  import { exec as exec30 } from "node:child_process";
106550
107034
  import { readFile as readFile21 } from "node:fs/promises";
106551
107035
  import { join as join44 } from "node:path";
@@ -106604,9 +107088,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
106604
107088
  return { attachmentContents, imageContents };
106605
107089
  }
106606
107090
  const { readFile: readFile29 } = await import("node:fs/promises");
106607
- const { join: join58 } = await import("node:path");
107091
+ const { join: join59 } = await import("node:path");
106608
107092
  for (const att of attachments) {
106609
- const filePath = join58(
107093
+ const filePath = join59(
106610
107094
  rootDir,
106611
107095
  ".fusion",
106612
107096
  "tasks",
@@ -108079,8 +108563,8 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
108079
108563
  });
108080
108564
  if (decision.shouldRetry) {
108081
108565
  const attempt = decision.nextState.recoveryRetryCount;
108082
- const delay4 = formatDelay(decision.delayMs);
108083
- const retryMessage = `Spec review not approved (${verdictDesc}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}.`;
108566
+ const delay5 = formatDelay(decision.delayMs);
108567
+ const retryMessage = `Spec review not approved (${verdictDesc}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}.`;
108084
108568
  planLog.warn(`${task.id} ${retryMessage}`);
108085
108569
  await this.store.logEntry(task.id, retryMessage);
108086
108570
  const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
@@ -108186,10 +108670,10 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
108186
108670
  });
108187
108671
  if (decision.shouldRetry) {
108188
108672
  const attempt = decision.nextState.recoveryRetryCount;
108189
- const delay4 = formatDelay(decision.delayMs);
108673
+ const delay5 = formatDelay(decision.delayMs);
108190
108674
  if (!isSilentTransientError(errorMessage)) {
108191
- planLog.warn(`\u26A1 ${task.id} transient error during triage \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
108192
- await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`).catch((err2) => {
108675
+ planLog.warn(`\u26A1 ${task.id} transient error during triage \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}: ${errorMessage}`);
108676
+ await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`).catch((err2) => {
108193
108677
  const msg = err2 instanceof Error ? err2.message : String(err2);
108194
108678
  planLog.warn(`${task.id}: failed to log transient-error retry entry: ${msg}`);
108195
108679
  });
@@ -108488,9 +108972,9 @@ Remove or replace these ids and call fn_task_create again.`
108488
108972
  }
108489
108973
  try {
108490
108974
  const { readFile: readFile29 } = await import("node:fs/promises");
108491
- const { join: join58 } = await import("node:path");
108975
+ const { join: join59 } = await import("node:path");
108492
108976
  const promptContent = await readFile29(
108493
- join58(rootDir, promptPath),
108977
+ join59(rootDir, promptPath),
108494
108978
  "utf-8"
108495
108979
  ).catch((err) => {
108496
108980
  const msg = err instanceof Error ? err.message : String(err);
@@ -109636,7 +110120,7 @@ var init_task_completion = __esm({
109636
110120
  import { spawn as spawn6 } from "node:child_process";
109637
110121
  import { existsSync as existsSync40 } from "node:fs";
109638
110122
  import { isAbsolute as isAbsolute13, join as join46 } from "node:path";
109639
- import { Type as Type4 } from "@mariozechner/pi-ai";
110123
+ import { Type as Type4 } from "@earendil-works/pi-ai";
109640
110124
  function createBuffer() {
109641
110125
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
109642
110126
  }
@@ -109945,8 +110429,8 @@ import { setImmediate as setImmediateCb3 } from "node:timers";
109945
110429
  import { delimiter, isAbsolute as isAbsolute14, join as join47, relative as relative11, resolve as resolvePath } from "node:path";
109946
110430
  import { existsSync as existsSync41, realpathSync as realpathSync6 } from "node:fs";
109947
110431
  import { readFile as readFile22, rm as rm5, writeFile as writeFile16 } from "node:fs/promises";
109948
- import { Type as Type5 } from "@mariozechner/pi-ai";
109949
- import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
110432
+ import { Type as Type5 } from "@earendil-works/pi-ai";
110433
+ import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
109950
110434
  function getResumeOrphanDelayMs() {
109951
110435
  const raw = process.env.FUSION_RESUME_ORPHAN_DELAY_MS;
109952
110436
  if (raw !== void 0) {
@@ -109963,6 +110447,26 @@ function canonicalizePath2(path6) {
109963
110447
  return resolvePath(path6);
109964
110448
  }
109965
110449
  }
110450
+ function isTransientMissingTaskJsonError(error, task) {
110451
+ if (error instanceof TaskDeletedError) {
110452
+ return false;
110453
+ }
110454
+ const message = typeof error === "string" ? error : error instanceof Error ? error.message : "";
110455
+ const match = TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN.exec(message);
110456
+ if (!match) {
110457
+ return false;
110458
+ }
110459
+ const [, filePath, taskIdFromPath] = match;
110460
+ if (taskIdFromPath !== task.id) {
110461
+ return false;
110462
+ }
110463
+ if (typeof task.worktree !== "string" || task.worktree.length === 0) {
110464
+ return false;
110465
+ }
110466
+ const normalizedWorktree = resolvePath(task.worktree);
110467
+ const normalizedTaskJsonPath = resolvePath(filePath);
110468
+ return normalizedTaskJsonPath.startsWith(`${normalizedWorktree}/`);
110469
+ }
109966
110470
  function detectPendingReviewBlock(task, codeReviewVerdicts) {
109967
110471
  const inProgressStepIndices = [];
109968
110472
  for (let stepIndex = 0; stepIndex < task.steps.length; stepIndex++) {
@@ -110498,7 +111002,7 @@ function detectReviewHandoffIntent(commentText) {
110498
111002
  ];
110499
111003
  return handoffPhrases.some((phrase) => text.includes(phrase));
110500
111004
  }
110501
- var execAsync30, yieldEventLoop3, tokenCacheMetricsLog, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, TASK_DONE_REFUSAL_SUFFIX, DISSENT_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, WORKFLOW_FEEDBACK_PATH_REGEX, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, WORKFLOW_STEP_VERDICTS, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
111005
+ var execAsync30, yieldEventLoop3, tokenCacheMetricsLog, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, TASK_DONE_REFUSAL_SUFFIX, TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN, DISSENT_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, WORKFLOW_FEEDBACK_PATH_REGEX, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, WORKFLOW_STEP_VERDICTS, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
110502
111006
  var init_executor = __esm({
110503
111007
  "../engine/src/executor.ts"() {
110504
111008
  "use strict";
@@ -110566,6 +111070,7 @@ var init_executor = __esm({
110566
111070
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
110567
111071
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
110568
111072
  TASK_DONE_REFUSAL_SUFFIX = "Either finish the work and resubmit, or do not call fn_task_done \u2014 exit the session and the engine will requeue.";
111073
+ TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN = /ENOENT:\s+no such file or directory,\s+open\s+'([^']+\/\.fusion\/tasks\/([^/]+)\/task\.json)'/;
110569
111074
  DISSENT_PATTERNS = [
110570
111075
  /\btask (is|was)(?: not|n['’]?t) complete\b/i,
110571
111076
  /\b(?:i (?:could|can)(?:not|n['’]?t)|unable to|failed to) (?:complete|finish|implement)\b/i,
@@ -113057,10 +113562,10 @@ ${summary}`,
113057
113562
  });
113058
113563
  if (decision.shouldRetry) {
113059
113564
  const attempt = decision.nextState.recoveryRetryCount;
113060
- const delay4 = formatDelay(decision.delayMs);
113565
+ const delay5 = formatDelay(decision.delayMs);
113061
113566
  if (!isSilentTransientError(errorMessage)) {
113062
- executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
113063
- await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
113567
+ executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}: ${errorMessage}`);
113568
+ await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
113064
113569
  }
113065
113570
  if (worktreePath && existsSync41(worktreePath)) {
113066
113571
  try {
@@ -113616,12 +114121,7 @@ ${summary}`,
113616
114121
  this.tokenUsageBaselines.delete(task.id);
113617
114122
  session.dispose();
113618
114123
  await this.persistTokenUsage(task.id);
113619
- await this.store.updateTask(task.id, {
113620
- status: "failed",
113621
- error: "executor-exit-while-review-pending"
113622
- });
113623
114124
  await this.handoffTaskToReview(task, "executor-exit-while-review-pending");
113624
- this.options.onError?.(task, new Error("executor-exit-while-review-pending"));
113625
114125
  pendingReviewParked = true;
113626
114126
  break;
113627
114127
  }
@@ -114022,9 +114522,9 @@ ${summary}`,
114022
114522
  });
114023
114523
  if (decision.shouldRetry) {
114024
114524
  const attempt = decision.nextState.recoveryRetryCount;
114025
- const delay4 = formatDelay(decision.delayMs);
114026
- executorLog.warn(`\u26A1 ${task.id} context-overflow fresh-session requeue ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}`);
114027
- await this.store.logEntry(task.id, `Context-overflow fresh-session requeue (${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114525
+ const delay5 = formatDelay(decision.delayMs);
114526
+ executorLog.warn(`\u26A1 ${task.id} context-overflow fresh-session requeue ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}`);
114527
+ await this.store.logEntry(task.id, `Context-overflow fresh-session requeue (${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114028
114528
  await this.store.updateTask(task.id, {
114029
114529
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
114030
114530
  nextRecoveryAt: decision.nextState.nextRecoveryAt,
@@ -114312,10 +114812,10 @@ ${summary}`,
114312
114812
  });
114313
114813
  if (decision.shouldRetry) {
114314
114814
  const attempt = decision.nextState.recoveryRetryCount;
114315
- const delay4 = formatDelay(decision.delayMs);
114815
+ const delay5 = formatDelay(decision.delayMs);
114316
114816
  if (!isSilentTransientError(errorMessage)) {
114317
- executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
114318
- await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114817
+ executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}: ${errorMessage}`);
114818
+ await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114319
114819
  }
114320
114820
  if (worktreePath && existsSync41(worktreePath)) {
114321
114821
  try {
@@ -117017,8 +117517,8 @@ Review the work done in this worktree and evaluate it against the criteria in yo
117017
117517
  `Failed to create worktree after ${this.MAX_WORKTREE_RETRIES} attempts: ${errorMessage}`
117018
117518
  );
117019
117519
  }
117020
- const delay4 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
117021
- await new Promise((resolve32) => setTimeout(resolve32, delay4));
117520
+ const delay5 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
117521
+ await new Promise((resolve32) => setTimeout(resolve32, delay5));
117022
117522
  }
117023
117523
  }
117024
117524
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -117256,9 +117756,15 @@ Review the work done in this worktree and evaluate it against the criteria in yo
117256
117756
  }
117257
117757
  async recoverMissingWorktreeSessionStartFailure(task, worktreePath, error, audit) {
117258
117758
  const errorText = error instanceof Error ? error.message : String(error);
117259
- if (!isMissingWorktreeSessionStartFailure(errorText)) return false;
117759
+ const missingWorktreeFailure = isMissingWorktreeSessionStartFailure(errorText);
117760
+ const missingTaskJsonFailure = isTransientMissingTaskJsonError(error, task);
117761
+ if (!missingWorktreeFailure && !missingTaskJsonFailure) return false;
117260
117762
  const classification = classifyMissingWorktreeSessionStartFailure(errorText);
117261
- const staleWorktreePath = extractMissingWorktreePathFromSessionStartFailure(errorText) ?? worktreePath;
117763
+ const missingTaskJsonPath = errorText.match(TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN)?.[1] ?? null;
117764
+ const staleWorktreePath = extractMissingWorktreePathFromSessionStartFailure(errorText) ?? (missingTaskJsonPath ? resolvePath(missingTaskJsonPath, "..", "..", "..") : null) ?? worktreePath;
117765
+ if (missingTaskJsonFailure) {
117766
+ executorLog.log(`[transient-task-json-suppressed] taskId=${task.id} elapsedMs=0 reason=missing-task-json-under-worktree path=${missingTaskJsonPath ?? "unknown"}`);
117767
+ }
117262
117768
  await audit.git({
117263
117769
  type: "worktree:incomplete-detected",
117264
117770
  target: staleWorktreePath,
@@ -120414,6 +120920,12 @@ var init_scheduler = __esm({
120414
120920
  }
120415
120921
  const sliceIdBeforeUpdate = feature.sliceId;
120416
120922
  if (this.options.missionExecutionLoop) {
120923
+ if (!this.options.missionExecutionLoop.isRunning()) {
120924
+ schedulerLog.warn(
120925
+ `MissionExecutionLoop was not running during task completion for ${taskId}; starting loop before processing outcome`
120926
+ );
120927
+ this.options.missionExecutionLoop.start();
120928
+ }
120417
120929
  void this.options.missionExecutionLoop.processTaskOutcome(taskId).catch((err) => {
120418
120930
  schedulerLog.error(`Error in missionExecutionLoop.processTaskOutcome for ${taskId}:`, err);
120419
120931
  });
@@ -120876,19 +121388,19 @@ var init_mission_autopilot = __esm({
120876
121388
  autopilotLog.error(`Error advancing slice for mission ${missionId}:`, err);
120877
121389
  state.retryCount++;
120878
121390
  if (state.retryCount <= MAX_RETRY_ATTEMPTS) {
120879
- const delay4 = RETRY_BASE_DELAY_MS2 * Math.pow(3, state.retryCount - 1);
121391
+ const delay5 = RETRY_BASE_DELAY_MS2 * Math.pow(3, state.retryCount - 1);
120880
121392
  this.logMissionEventSafe(
120881
121393
  missionId,
120882
121394
  "autopilot_retry",
120883
121395
  `Retrying slice activation after error (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS})`,
120884
- { retryCount: state.retryCount, maxRetries: MAX_RETRY_ATTEMPTS, delayMs: delay4 }
121396
+ { retryCount: state.retryCount, maxRetries: MAX_RETRY_ATTEMPTS, delayMs: delay5 }
120885
121397
  );
120886
- autopilotLog.log(`Retrying slice activation for mission ${missionId} (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS}, delay ${delay4}ms)`);
121398
+ autopilotLog.log(`Retrying slice activation for mission ${missionId} (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS}, delay ${delay5}ms)`);
120887
121399
  setTimeout(() => {
120888
121400
  if (this.isWatching(missionId)) {
120889
121401
  void this.advanceToNextSlice(missionId);
120890
121402
  }
120891
- }, delay4);
121403
+ }, delay5);
120892
121404
  } else {
120893
121405
  this.logMissionEventSafe(
120894
121406
  missionId,
@@ -121335,7 +121847,7 @@ var init_mission_autopilot = __esm({
121335
121847
  });
121336
121848
 
121337
121849
  // ../engine/src/mission-execution-loop.ts
121338
- import { EventEmitter as EventEmitter18 } from "node:events";
121850
+ import { EventEmitter as EventEmitter19 } from "node:events";
121339
121851
  var loopLog, VALIDATION_TIMEOUT_MS, MissionExecutionLoop;
121340
121852
  var init_mission_execution_loop = __esm({
121341
121853
  "../engine/src/mission-execution-loop.ts"() {
@@ -121347,7 +121859,7 @@ var init_mission_execution_loop = __esm({
121347
121859
  init_run_audit();
121348
121860
  loopLog = createLogger2("mission-loop");
121349
121861
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
121350
- MissionExecutionLoop = class extends EventEmitter18 {
121862
+ MissionExecutionLoop = class extends EventEmitter19 {
121351
121863
  running = false;
121352
121864
  taskStore;
121353
121865
  missionStore;
@@ -121432,7 +121944,10 @@ var init_mission_execution_loop = __esm({
121432
121944
  try {
121433
121945
  await this.missionStore.transitionLoopState(feature.id, "implementing");
121434
121946
  if (feature.taskId) {
121435
- await this.processTaskOutcome(feature.taskId);
121947
+ const linkedTask = await this.taskStore.getTask(feature.taskId).catch(() => null);
121948
+ if (linkedTask && (linkedTask.column === "done" || linkedTask.column === "archived")) {
121949
+ await this.processTaskOutcome(feature.taskId);
121950
+ }
121436
121951
  }
121437
121952
  recoveredCount++;
121438
121953
  } catch (err) {
@@ -121443,7 +121958,10 @@ var init_mission_execution_loop = __esm({
121443
121958
  loopLog.log(`Recovery: feature ${feature.id} awaiting fix implementation`);
121444
121959
  if (feature.taskId) {
121445
121960
  try {
121446
- await this.processTaskOutcome(feature.taskId);
121961
+ const linkedTask = await this.taskStore.getTask(feature.taskId).catch(() => null);
121962
+ if (linkedTask && (linkedTask.column === "done" || linkedTask.column === "archived")) {
121963
+ await this.processTaskOutcome(feature.taskId);
121964
+ }
121447
121965
  recoveredCount++;
121448
121966
  } catch (err) {
121449
121967
  loopLog.error(`Recovery failed for needs_fix feature ${feature.id}:`, err);
@@ -121485,6 +122003,17 @@ var init_mission_execution_loop = __esm({
121485
122003
  }
121486
122004
  if (feature.loopState !== "implementing") {
121487
122005
  loopLog.log(`Feature ${feature.id} loopState is "${feature.loopState}"; skipping validation`);
122006
+ this.logFeatureWarningEvent(feature.id, "validation_skipped_loop_state", `Validation skipped: feature ${feature.id} is in loopState "${feature.loopState}" (expected "implementing").`, {
122007
+ taskId,
122008
+ loopState: feature.loopState
122009
+ });
122010
+ return;
122011
+ }
122012
+ if (this.activeValidations.has(feature.id)) {
122013
+ loopLog.log(`Feature ${feature.id} already has an active validation; skipping duplicate trigger`);
122014
+ this.logFeatureWarningEvent(feature.id, "validation_deduplicated", `Validation already running for feature ${feature.id}; duplicate trigger ignored.`, {
122015
+ taskId
122016
+ });
121488
122017
  return;
121489
122018
  }
121490
122019
  const assertions = this.missionStore.listAssertionsForFeature(feature.id);
@@ -121875,6 +122404,10 @@ ${taskContext}` : ""}`;
121875
122404
  if (runId) {
121876
122405
  this.missionStore.completeValidatorRun(runId, "passed", summary);
121877
122406
  }
122407
+ const feature = this.missionStore.getFeature(featureId);
122408
+ if (feature && feature.status !== "done") {
122409
+ this.missionStore.updateFeatureStatus(featureId, "done");
122410
+ }
121878
122411
  loopLog.log(`Feature ${featureId} passed validation`);
121879
122412
  if (this.missionAutopilot?.notifyValidationComplete) {
121880
122413
  await this.missionAutopilot.notifyValidationComplete(featureId, "passed");
@@ -121948,6 +122481,10 @@ ${taskContext}` : ""}`;
121948
122481
  this.missionStore.completeValidatorRun(runId, "blocked", blockedReason);
121949
122482
  }
121950
122483
  loopLog.log(`Feature ${featureId} blocked: ${blockedReason}`);
122484
+ this.logFeatureErrorEvent(featureId, "validation_blocked", `Validation blocked for feature ${featureId}: ${blockedReason ?? "no reason provided"}`, {
122485
+ runId,
122486
+ blockedReason: blockedReason ?? null
122487
+ });
121951
122488
  if (this.missionAutopilot?.notifyValidationComplete) {
121952
122489
  await this.missionAutopilot.notifyValidationComplete(featureId, "blocked");
121953
122490
  }
@@ -121965,6 +122502,10 @@ ${taskContext}` : ""}`;
121965
122502
  this.missionStore.completeValidatorRun(runId, "error", error);
121966
122503
  }
121967
122504
  loopLog.error(`Feature ${featureId} validation error: ${error}`);
122505
+ this.logFeatureErrorEvent(featureId, "validation_error", `Validation error for feature ${featureId}: ${error}`, {
122506
+ runId,
122507
+ error
122508
+ });
121968
122509
  if (this.missionAutopilot?.notifyValidationComplete) {
121969
122510
  await this.missionAutopilot.notifyValidationComplete(featureId, "error");
121970
122511
  }
@@ -121973,6 +122514,31 @@ ${taskContext}` : ""}`;
121973
122514
  loopLog.error(`Error handling validation error for ${featureId}:`, err);
121974
122515
  }
121975
122516
  }
122517
+ logFeatureWarningEvent(featureId, code, description, metadata) {
122518
+ this.logFeatureMissionEvent(featureId, "warning", code, description, metadata);
122519
+ }
122520
+ logFeatureErrorEvent(featureId, code, description, metadata) {
122521
+ this.logFeatureMissionEvent(featureId, "error", code, description, metadata);
122522
+ }
122523
+ logFeatureMissionEvent(featureId, eventType, code, description, metadata) {
122524
+ const feature = this.missionStore.getFeature(featureId);
122525
+ if (!feature) return;
122526
+ const slice = this.missionStore.getSlice(feature.sliceId);
122527
+ if (!slice) return;
122528
+ const milestone = this.missionStore.getMilestone(slice.milestoneId);
122529
+ if (!milestone) return;
122530
+ try {
122531
+ this.missionStore.logMissionEvent?.(milestone.missionId, eventType, description, {
122532
+ code,
122533
+ featureId,
122534
+ sliceId: slice.id,
122535
+ milestoneId: milestone.id,
122536
+ ...metadata
122537
+ });
122538
+ } catch (err) {
122539
+ loopLog.warn(`Failed to log mission ${eventType} event for feature ${featureId}:`, err);
122540
+ }
122541
+ }
121976
122542
  };
121977
122543
  }
121978
122544
  });
@@ -123085,7 +123651,7 @@ var init_room_coordination = __esm({
123085
123651
  });
123086
123652
 
123087
123653
  // ../engine/src/agent-heartbeat.ts
123088
- import { Type as Type6 } from "@mariozechner/pi-ai";
123654
+ import { Type as Type6 } from "@earendil-works/pi-ai";
123089
123655
  import { createHash as createHash12 } from "node:crypto";
123090
123656
  function adjustHeartbeatMemoryPrimer(basePrompt, mode) {
123091
123657
  if (mode === "full") return basePrompt;
@@ -126435,6 +127001,78 @@ var init_error_classifier = __esm({
126435
127001
  }
126436
127002
  });
126437
127003
 
127004
+ // ../engine/src/goal-context-injector.ts
127005
+ function normalizeTitle(title) {
127006
+ return title.replace(/\s*\n+\s*/g, " ").trim();
127007
+ }
127008
+ function renderGoalContext(goals) {
127009
+ const lines = goals.map((goal) => `- ${goal.id}: ${normalizeTitle(goal.title)}`);
127010
+ return `## Active Goals
127011
+
127012
+ ${lines.join("\n")}`;
127013
+ }
127014
+ function buildGoalContextSection(input) {
127015
+ const { activeGoals, onTruncated } = input;
127016
+ if (activeGoals.length === 0) {
127017
+ return { text: "", emittedGoalIds: [], truncated: null };
127018
+ }
127019
+ const sortedGoals = [...activeGoals].sort((a, b2) => {
127020
+ const byCreated = a.createdAt.localeCompare(b2.createdAt);
127021
+ if (byCreated !== 0) {
127022
+ return byCreated;
127023
+ }
127024
+ return a.id.localeCompare(b2.id);
127025
+ });
127026
+ const resolvedMaxGoals = Math.min(input.maxGoals ?? MAX_INJECTED_GOALS, MAX_INJECTED_GOALS);
127027
+ const charBudget = input.charBudget ?? DEFAULT_GOAL_INJECTION_CHAR_BUDGET;
127028
+ const droppedGoalIds = [];
127029
+ let selectedGoals = sortedGoals.slice(0, resolvedMaxGoals);
127030
+ if (sortedGoals.length > resolvedMaxGoals) {
127031
+ droppedGoalIds.push(...sortedGoals.slice(resolvedMaxGoals).map((goal) => goal.id));
127032
+ }
127033
+ let text = renderGoalContext(selectedGoals);
127034
+ let usedBudgetDrop = false;
127035
+ while (selectedGoals.length > 1 && text.length > charBudget) {
127036
+ const dropped = selectedGoals[selectedGoals.length - 1];
127037
+ selectedGoals = selectedGoals.slice(0, -1);
127038
+ droppedGoalIds.push(dropped.id);
127039
+ text = renderGoalContext(selectedGoals);
127040
+ usedBudgetDrop = true;
127041
+ }
127042
+ let truncated = null;
127043
+ if (droppedGoalIds.length > 0) {
127044
+ truncated = {
127045
+ reason: usedBudgetDrop ? "budget" : "cap",
127046
+ requested: activeGoals.length,
127047
+ emitted: selectedGoals.length,
127048
+ droppedGoalIds,
127049
+ charBudget,
127050
+ producedChars: text.length
127051
+ };
127052
+ onTruncated?.(truncated);
127053
+ }
127054
+ return {
127055
+ text,
127056
+ emittedGoalIds: selectedGoals.map((goal) => goal.id),
127057
+ truncated
127058
+ };
127059
+ }
127060
+ var MAX_INJECTED_GOALS, resolvedActiveGoalLimit, DEFAULT_GOAL_INJECTION_CHAR_BUDGET;
127061
+ var init_goal_context_injector = __esm({
127062
+ "../engine/src/goal-context-injector.ts"() {
127063
+ "use strict";
127064
+ init_src();
127065
+ MAX_INJECTED_GOALS = 5;
127066
+ resolvedActiveGoalLimit = "ACTIVE_GOAL_LIMIT" in src_exports && typeof ACTIVE_GOAL_LIMIT === "number" ? ACTIVE_GOAL_LIMIT : MAX_INJECTED_GOALS;
127067
+ if (MAX_INJECTED_GOALS > resolvedActiveGoalLimit) {
127068
+ throw new Error(
127069
+ `MAX_INJECTED_GOALS (${MAX_INJECTED_GOALS}) cannot exceed ACTIVE_GOAL_LIMIT (${resolvedActiveGoalLimit})`
127070
+ );
127071
+ }
127072
+ DEFAULT_GOAL_INJECTION_CHAR_BUDGET = 600;
127073
+ }
127074
+ });
127075
+
126438
127076
  // ../engine/src/experiment/benchmark-runner.ts
126439
127077
  import { spawn as spawn7 } from "node:child_process";
126440
127078
  import { mkdtemp, writeFile as writeFile17 } from "node:fs/promises";
@@ -130216,7 +130854,7 @@ var init_stuck_task_detector = __esm({
130216
130854
  });
130217
130855
 
130218
130856
  // ../engine/src/plugin-runner.ts
130219
- import { Type as Type7 } from "@mariozechner/pi-ai";
130857
+ import { Type as Type7 } from "@earendil-works/pi-ai";
130220
130858
  import { isAbsolute as isAbsolute16 } from "node:path";
130221
130859
  var DEFAULT_HOOK_TIMEOUT_MS, PluginRunner;
130222
130860
  var init_plugin_runner = __esm({
@@ -131367,7 +132005,7 @@ var init_task_agent_sync = __esm({
131367
132005
  });
131368
132006
 
131369
132007
  // ../engine/src/runtimes/in-process-runtime.ts
131370
- import { EventEmitter as EventEmitter19 } from "node:events";
132008
+ import { EventEmitter as EventEmitter20 } from "node:events";
131371
132009
  import { setImmediate as setImmediateCb4 } from "node:timers";
131372
132010
  var yieldEventLoop4, InProcessRuntime;
131373
132011
  var init_in_process_runtime = __esm({
@@ -131397,7 +132035,7 @@ var init_in_process_runtime = __esm({
131397
132035
  init_task_agent_sync();
131398
132036
  init_run_audit();
131399
132037
  yieldEventLoop4 = () => new Promise((resolve32) => setImmediateCb4(resolve32));
131400
- InProcessRuntime = class extends EventEmitter19 {
132038
+ InProcessRuntime = class extends EventEmitter20 {
131401
132039
  /**
131402
132040
  * @param config - Runtime configuration
131403
132041
  * @param centralCore - CentralCore reference for global coordination
@@ -131568,9 +132206,19 @@ var init_in_process_runtime = __esm({
131568
132206
  missionAutopilot: missionAutopilot ? {
131569
132207
  notifyValidationComplete: async (featureId) => {
131570
132208
  const feature = missionStore.getFeature(featureId);
131571
- if (feature?.taskId) {
131572
- await missionAutopilot.handleTaskCompletion(feature.taskId);
132209
+ if (!feature?.taskId) {
132210
+ return;
132211
+ }
132212
+ const slice = missionStore.getSlice(feature.sliceId);
132213
+ const milestone = slice ? missionStore.getMilestone(slice.milestoneId) : void 0;
132214
+ const missionId = milestone?.missionId;
132215
+ if (missionId) {
132216
+ const mission = missionStore.getMission(missionId);
132217
+ if (mission?.autopilotEnabled && !missionAutopilot.isWatching(missionId)) {
132218
+ missionAutopilot.watchMission(missionId);
132219
+ }
131573
132220
  }
132221
+ await missionAutopilot.handleTaskCompletion(feature.taskId);
131574
132222
  }
131575
132223
  } : void 0,
131576
132224
  rootDir: this.config.workingDirectory,
@@ -132427,14 +133075,14 @@ var init_ipc_protocol = __esm({
132427
133075
  });
132428
133076
 
132429
133077
  // ../engine/src/ipc/ipc-host.ts
132430
- import { EventEmitter as EventEmitter20 } from "node:events";
133078
+ import { EventEmitter as EventEmitter21 } from "node:events";
132431
133079
  var IpcHost;
132432
133080
  var init_ipc_host = __esm({
132433
133081
  "../engine/src/ipc/ipc-host.ts"() {
132434
133082
  "use strict";
132435
133083
  init_ipc_protocol();
132436
133084
  init_logger2();
132437
- IpcHost = class extends EventEmitter20 {
133085
+ IpcHost = class extends EventEmitter21 {
132438
133086
  /**
132439
133087
  * @param childProcess - The forked child process to communicate with
132440
133088
  * @param options - Optional configuration
@@ -132624,7 +133272,7 @@ var child_process_runtime_exports = {};
132624
133272
  __export(child_process_runtime_exports, {
132625
133273
  ChildProcessRuntime: () => ChildProcessRuntime
132626
133274
  });
132627
- import { EventEmitter as EventEmitter21 } from "node:events";
133275
+ import { EventEmitter as EventEmitter22 } from "node:events";
132628
133276
  import { fork } from "node:child_process";
132629
133277
  import { fileURLToPath as fileURLToPath3 } from "node:url";
132630
133278
  import { dirname as dirname18, join as join49 } from "node:path";
@@ -132681,8 +133329,8 @@ var init_child_process_runtime = __esm({
132681
133329
  runtimeLog.log("Health monitor stopped");
132682
133330
  }
132683
133331
  getRestartDelay() {
132684
- const delay4 = this.restartDelays[this.restartAttempts] ?? this.restartDelays[this.restartDelays.length - 1];
132685
- return delay4;
133332
+ const delay5 = this.restartDelays[this.restartAttempts] ?? this.restartDelays[this.restartDelays.length - 1];
133333
+ return delay5;
132686
133334
  }
132687
133335
  incrementRestartAttempts() {
132688
133336
  this.restartAttempts++;
@@ -132694,7 +133342,7 @@ var init_child_process_runtime = __esm({
132694
133342
  return this.missedHeartbeats;
132695
133343
  }
132696
133344
  };
132697
- ChildProcessRuntime = class extends EventEmitter21 {
133345
+ ChildProcessRuntime = class extends EventEmitter22 {
132698
133346
  /**
132699
133347
  * @param config - Runtime configuration
132700
133348
  * @param centralCore - CentralCore reference for global coordination
@@ -132963,9 +133611,9 @@ var init_child_process_runtime = __esm({
132963
133611
  this.emit("error", new Error("Child process failed after max restart attempts"));
132964
133612
  return;
132965
133613
  }
132966
- const delay4 = this.healthMonitor.getRestartDelay();
133614
+ const delay5 = this.healthMonitor.getRestartDelay();
132967
133615
  this.healthMonitor.incrementRestartAttempts();
132968
- runtimeLog.log(`Attempting restart ${this.healthMonitor.getRestartAttempts()}/${maxRestarts} after ${delay4}ms`);
133616
+ runtimeLog.log(`Attempting restart ${this.healthMonitor.getRestartAttempts()}/${maxRestarts} after ${delay5}ms`);
132969
133617
  const gen = this.generation;
132970
133618
  this.restartTimer = setTimeout(async () => {
132971
133619
  this.restartTimer = null;
@@ -132984,7 +133632,7 @@ var init_child_process_runtime = __esm({
132984
133632
  this.setStatus("errored");
132985
133633
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
132986
133634
  }
132987
- }, delay4);
133635
+ }, delay5);
132988
133636
  }
132989
133637
  /**
132990
133638
  * Handle child process exit.
@@ -133379,14 +134027,14 @@ var init_remote_node_client = __esm({
133379
134027
  });
133380
134028
 
133381
134029
  // ../engine/src/runtimes/remote-node-runtime.ts
133382
- import { EventEmitter as EventEmitter22 } from "node:events";
134030
+ import { EventEmitter as EventEmitter23 } from "node:events";
133383
134031
  var RemoteNodeRuntime;
133384
134032
  var init_remote_node_runtime = __esm({
133385
134033
  "../engine/src/runtimes/remote-node-runtime.ts"() {
133386
134034
  "use strict";
133387
134035
  init_logger2();
133388
134036
  init_remote_node_client();
133389
- RemoteNodeRuntime = class extends EventEmitter22 {
134037
+ RemoteNodeRuntime = class extends EventEmitter23 {
133390
134038
  constructor(config) {
133391
134039
  super();
133392
134040
  this.config = config;
@@ -133696,7 +134344,7 @@ var init_remote_node_runtime = __esm({
133696
134344
  });
133697
134345
 
133698
134346
  // ../engine/src/project-manager.ts
133699
- import { EventEmitter as EventEmitter23 } from "node:events";
134347
+ import { EventEmitter as EventEmitter24 } from "node:events";
133700
134348
  var ProjectManager;
133701
134349
  var init_project_manager = __esm({
133702
134350
  "../engine/src/project-manager.ts"() {
@@ -133706,7 +134354,7 @@ var init_project_manager = __esm({
133706
134354
  init_remote_node_runtime();
133707
134355
  init_concurrency();
133708
134356
  init_logger2();
133709
- ProjectManager = class extends EventEmitter23 {
134357
+ ProjectManager = class extends EventEmitter24 {
133710
134358
  /**
133711
134359
  * @param centralCore - CentralCore reference for global coordination
133712
134360
  */
@@ -134173,6 +134821,661 @@ var init_gridlock_detector = __esm({
134173
134821
  }
134174
134822
  });
134175
134823
 
134824
+ // ../engine/src/merger-ai.ts
134825
+ import { execFile as execFile15 } from "node:child_process";
134826
+ import { promisify as promisify43 } from "node:util";
134827
+ import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
134828
+ import { tmpdir as tmpdir5 } from "node:os";
134829
+ import { join as join50 } from "node:path";
134830
+ async function git2(args, cwd, opts = {}) {
134831
+ const { stdout } = await execFileAsync10("git", args, {
134832
+ cwd,
134833
+ encoding: "utf-8",
134834
+ timeout: opts.timeout ?? 12e4,
134835
+ maxBuffer: 16 * 1024 * 1024
134836
+ });
134837
+ return stdout.trim();
134838
+ }
134839
+ async function gitOk(args, cwd) {
134840
+ try {
134841
+ await git2(args, cwd);
134842
+ return true;
134843
+ } catch {
134844
+ return false;
134845
+ }
134846
+ }
134847
+ function taskTrailers(taskId, lineageId) {
134848
+ const trailers = [`${FUSION_TASK_ID_TRAILER_KEY3}: ${taskId}`];
134849
+ if (lineageId) trailers.push(buildTaskLineageTrailer(lineageId));
134850
+ return trailers;
134851
+ }
134852
+ async function ensureCommitTaskMetadata(mergeRoot, taskId, includeTaskId, trailers) {
134853
+ const fullMessage = await git2(["log", "-1", "--pretty=%B"], mergeRoot).catch(() => "");
134854
+ if (!fullMessage) return;
134855
+ const subject = (fullMessage.split("\n")[0] ?? "").trim();
134856
+ const body = await git2(["log", "-1", "--pretty=%b"], mergeRoot).catch(() => "");
134857
+ const needsPrefix = includeTaskId && !subject.toLowerCase().startsWith(taskId.toLowerCase());
134858
+ const missingTrailers = trailers.filter((t) => !fullMessage.includes(t));
134859
+ if (!needsPrefix && missingTrailers.length === 0) return;
134860
+ const args = ["-c", "trailer.ifExists=addIfDifferent", "commit", "--amend"];
134861
+ if (needsPrefix) {
134862
+ args.push("-m", `${taskId}: ${subject}`);
134863
+ if (body.trim()) args.push("-m", body);
134864
+ } else {
134865
+ args.push("--no-edit");
134866
+ }
134867
+ for (const t of missingTrailers) args.push("--trailer", t);
134868
+ await git2(args, mergeRoot).catch((err) => {
134869
+ aiMergeLog.warn(`failed to amend task metadata onto squash (${err instanceof Error ? err.message : String(err)})`);
134870
+ });
134871
+ }
134872
+ function parseReviewVerdict(agentText) {
134873
+ const text = (agentText ?? "").trim();
134874
+ if (!text) return { verdict: "reject", reasons: ["reviewer produced no output"], severity: "blocking" };
134875
+ const lines = text.split(/\r?\n/);
134876
+ let verdictLineIndex = -1;
134877
+ let decision = null;
134878
+ for (let i = lines.length - 1; i >= 0; i--) {
134879
+ const m2 = lines[i].match(VERDICT_LINE_RE);
134880
+ if (m2) {
134881
+ decision = m2[1].toLowerCase();
134882
+ verdictLineIndex = i;
134883
+ break;
134884
+ }
134885
+ }
134886
+ if (!decision) {
134887
+ return {
134888
+ verdict: "reject",
134889
+ reasons: [`reviewer did not emit a "${REVIEW_VERDICT_MARKER} approve|reject" line`],
134890
+ severity: "blocking"
134891
+ };
134892
+ }
134893
+ if (decision === "approve") return { verdict: "approve", reasons: [] };
134894
+ const severity = SEVERITY_LINE_RE.test(text) ? text.match(SEVERITY_LINE_RE)[1].toLowerCase() : "blocking";
134895
+ return { verdict: "reject", reasons: extractRejectReasons(lines, verdictLineIndex), severity };
134896
+ }
134897
+ function extractRejectReasons(lines, verdictLineIndex) {
134898
+ const reasons = [];
134899
+ const inline = lines[verdictLineIndex].replace(VERDICT_LINE_RE, "").replace(/^[\s:–—-]+/, "").trim();
134900
+ if (inline) reasons.push(inline);
134901
+ for (let i = verdictLineIndex + 1; i < lines.length; i++) {
134902
+ if (SEVERITY_LINE_RE.test(lines[i])) continue;
134903
+ const cleaned = lines[i].replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").trim();
134904
+ if (cleaned) reasons.push(cleaned);
134905
+ }
134906
+ if (reasons.length === 0) reasons.push("reviewer rejected the merge without a stated reason");
134907
+ return reasons;
134908
+ }
134909
+ function buildMergeSystemPrompt2(agentPrompts) {
134910
+ const base = resolveAgentPrompt("merger", agentPrompts).trim();
134911
+ return [
134912
+ base,
134913
+ base ? "" : void 0,
134914
+ "## AI merge \u2014 clean room",
134915
+ "You are on a CLEAN, detached checkout at the integration branch's current",
134916
+ "tip. Land the task branch's work as a single commit.",
134917
+ "",
134918
+ "Constraints:",
134919
+ " - Resolve every conflict in favor of the task branch's intent; never drop",
134920
+ " the task's changes to make a conflict go away.",
134921
+ " - Do not make edits unrelated to reconciling the two branches.",
134922
+ " - Do NOT push, force-push, or run `git update-ref` / `git reset --hard`",
134923
+ " on any other branch. Only commit on this detached HEAD.",
134924
+ " - Finish with exactly ONE new commit on HEAD containing the task's work.",
134925
+ "",
134926
+ "Verify before committing:",
134927
+ " - After resolving the merge, run the project's checks \u2014 tests, type-check,",
134928
+ " and lint (discover them from the project config / package.json scripts,",
134929
+ " e.g. test / typecheck / lint / build).",
134930
+ " - FIX any NEW failure the merge or conflict resolution introduced (a check",
134931
+ " that passed on the task branch or the integration tip but fails on the",
134932
+ " merged tree). You do not need to fix failures that were already broken on",
134933
+ " the integration branch beforehand, but never commit a merge that adds new",
134934
+ " test, type-check, or lint failures.",
134935
+ "",
134936
+ "Commit message:",
134937
+ " - The subject line must CONCISELY SUMMARIZE the squashed changes in",
134938
+ ' imperative mood (e.g. "add X", "fix Y") based on the actual diff \u2014 do',
134939
+ " not just restate the task title.",
134940
+ " - The commit BODY must include:",
134941
+ " 1) one short narrative summary line,",
134942
+ " 2) a bullet list of key changes, and",
134943
+ " 3) a `Files changed:` section populated from `git diff --stat`.",
134944
+ " - Include the task-id prefix and the trailer lines EXACTLY as given in the",
134945
+ " task instructions (they associate the commit with the board task)."
134946
+ ].filter((l) => l !== void 0).join("\n");
134947
+ }
134948
+ function buildMergePrompt2(input) {
134949
+ const subjectShape = input.includeTaskId ? `"${input.taskId}: <concise imperative summary of the squashed changes>"` : `"<concise imperative summary of the squashed changes>"`;
134950
+ const trailerArgs = input.trailers.map((t) => ` -m ${JSON.stringify(t)}`).join("");
134951
+ const lines = [
134952
+ `Merge branch "${input.branch}" into "${input.integrationBranch}" (HEAD is detached at ${short(input.tipSha)}).`,
134953
+ "",
134954
+ "Steps:",
134955
+ ` 1. Run: git merge --squash ${input.branch}`,
134956
+ " 2. If there are conflicts, resolve them (favor the task's intent), then `git add` the resolved files.",
134957
+ " 3. Build a merge body from the staged squash diff:",
134958
+ " - one short narrative summary line",
134959
+ " - bullet list of key changes",
134960
+ " - `Files changed:` + the output of `git diff --stat`",
134961
+ " 4. Commit the staged result as a SINGLE commit whose subject summarizes the",
134962
+ ` actual changes${input.taskTitle ? ` (task title hint: ${JSON.stringify(input.taskTitle)})` : ""}, including the body above and required trailers:`,
134963
+ ` git commit -m ${subjectShape} -m "<narrative + bullet list + Files changed: ...>"${trailerArgs}`,
134964
+ " Keep the trailer line(s) verbatim \u2014 they link the commit to the board task.",
134965
+ " 5. Verify `git log --oneline ${tip}..HEAD` shows exactly one new commit and `git status` is clean.".replace("${tip}", short(input.tipSha)),
134966
+ "",
134967
+ "If `git merge --squash` reports the branch is already up to date (nothing to",
134968
+ "merge), do nothing and leave HEAD unchanged."
134969
+ ];
134970
+ if (input.correctiveReasons && input.correctiveReasons.length > 0) {
134971
+ lines.push(
134972
+ "",
134973
+ "A prior attempt was REJECTED by review. Redo the merge from the clean tip",
134974
+ "and address each of these problems:",
134975
+ ...input.correctiveReasons.map((r) => ` - ${r}`)
134976
+ );
134977
+ }
134978
+ return lines.join("\n");
134979
+ }
134980
+ function buildReviewSystemPrompt() {
134981
+ return [
134982
+ "You are an adversarial, read-only merge reviewer. Do NOT edit, stage, commit,",
134983
+ "or run any mutating git command. Audit the squash commit that is about to be",
134984
+ "merged into the integration branch and decide whether it is safe to land.",
134985
+ "",
134986
+ "Investigate with read-only commands (git show, git diff, git log, cat, grep).",
134987
+ "Judge on four axes:",
134988
+ " 1. Completeness \u2014 does the squash contain ALL of the task branch's intended",
134989
+ " changes? Flag any hunk silently dropped during conflict resolution.",
134990
+ " 2. No collateral \u2014 does it touch only files within the task's footprint?",
134991
+ " 3. Conflict soundness \u2014 were conflicts resolved coherently (both sides'",
134992
+ " intent preserved), not by blindly discarding one side?",
134993
+ " 4. Commit message \u2014 read `git show`'s message: the subject must concisely",
134994
+ " and ACCURATELY summarize the actual changes (not vague, not a mere",
134995
+ " restatement of the task title, not misleading). A poor/inaccurate",
134996
+ " message is an ADVISORY concern (it should be rewritten on retry, but",
134997
+ " must not block the merge).",
134998
+ "",
134999
+ "Bias toward rejection when uncertain.",
135000
+ "",
135001
+ `End with a single decision line: "${REVIEW_VERDICT_MARKER} approve" or`,
135002
+ `"${REVIEW_VERDICT_MARKER} reject". When rejecting, add a "SEVERITY:" line:`,
135003
+ " - SEVERITY: blocking \u2014 a correctness problem (dropped/lost task changes,",
135004
+ " incomplete squash, or a conflict resolution that discards intent). The",
135005
+ " merge must NOT land if this is unfixable.",
135006
+ " - SEVERITY: advisory \u2014 a quality/style concern that does not risk",
135007
+ " correctness; acceptable to land if unresolved.",
135008
+ "Then list each concrete reason as a bullet."
135009
+ ].join("\n");
135010
+ }
135011
+ function buildReviewPrompt(input) {
135012
+ const lines = [
135013
+ `Review the squash merge for task ${input.taskId} (branch ${input.branch} \u2192 ${input.integrationBranch}).`,
135014
+ "",
135015
+ `Integration tip: ${short(input.tipSha)}`,
135016
+ `Squash commit: ${short(input.squashSha)}`,
135017
+ "",
135018
+ "Inspect with:",
135019
+ ` git show ${input.squashSha}`,
135020
+ ` git diff ${input.tipSha}..${input.squashSha}`,
135021
+ "",
135022
+ "Files changed (git diff --stat):",
135023
+ input.diffStat.trim() || "(none reported)"
135024
+ ];
135025
+ if (input.priorReasons && input.priorReasons.length > 0) {
135026
+ lines.push(
135027
+ "",
135028
+ "A prior pass rejected an earlier attempt for these reasons \u2014 confirm they",
135029
+ "are now resolved:",
135030
+ ...input.priorReasons.map((r) => ` - ${r}`)
135031
+ );
135032
+ }
135033
+ return lines.join("\n");
135034
+ }
135035
+ function buildStashResolveSystemPrompt() {
135036
+ return [
135037
+ "You are resolving a conflict between the user's restored local working-tree",
135038
+ "edits and the freshly-merged integration branch. The user's uncommitted work",
135039
+ "was stashed, the checkout fast-forwarded to the new tip, and re-applying the",
135040
+ "stash produced conflicts.",
135041
+ "",
135042
+ "Resolve every conflict marker so BOTH sides are preserved: keep the user's",
135043
+ "local intent AND the upstream changes that just landed. Stage each resolved",
135044
+ "file with `git add`.",
135045
+ "",
135046
+ "Do NOT commit, stash, reset, checkout a different branch, or run update-ref.",
135047
+ "Leave the resolved changes in the working tree as the user's uncommitted edits."
135048
+ ].join("\n");
135049
+ }
135050
+ function buildStashResolvePrompt(conflictedFiles) {
135051
+ return [
135052
+ "Re-applying your stashed local changes onto the updated branch conflicted.",
135053
+ "",
135054
+ "Conflicted files:",
135055
+ ...conflictedFiles.map((f2) => ` - ${f2}`),
135056
+ "",
135057
+ "Resolve each file's conflict markers (preserve both the local edits and the",
135058
+ "upstream changes), then `git add` it. Do not commit."
135059
+ ].join("\n");
135060
+ }
135061
+ function short(sha) {
135062
+ return /^[0-9a-f]{7,40}$/i.test(sha) ? sha.slice(0, 8) : sha;
135063
+ }
135064
+ function makeMutatingAgent(store, settings, taskId, options, audit, systemPrompt) {
135065
+ return async (cwd, prompt) => {
135066
+ const model = resolveMergerSessionModel(settings);
135067
+ const { session } = await createResolvedAgentSession({
135068
+ sessionPurpose: "merger",
135069
+ pluginRunner: options.pluginRunner,
135070
+ cwd,
135071
+ systemPrompt,
135072
+ tools: "coding",
135073
+ onText: options.onAgentText ? (delta) => options.onAgentText?.(delta) : void 0,
135074
+ defaultProvider: model.provider,
135075
+ defaultModelId: model.modelId,
135076
+ fallbackProvider: settings.fallbackProvider,
135077
+ fallbackModelId: settings.fallbackModelId,
135078
+ defaultThinkingLevel: settings.defaultThinkingLevel,
135079
+ runAuditor: audit,
135080
+ settings,
135081
+ taskId
135082
+ });
135083
+ options.onSession?.(session);
135084
+ try {
135085
+ await withRateLimitRetry(async () => {
135086
+ await promptWithFallback(session, prompt);
135087
+ checkSessionError(session);
135088
+ }, { signal: options.signal });
135089
+ await accumulateSessionTokenUsage(store, taskId, session);
135090
+ } finally {
135091
+ session.dispose();
135092
+ }
135093
+ };
135094
+ }
135095
+ function makeReviewAgent(store, settings, taskId, options, audit) {
135096
+ return async (cwd, prompt) => {
135097
+ const validator = resolveValidatorSettingsModel(settings);
135098
+ const model = validator.provider && validator.modelId ? validator : resolveMergerSessionModel(settings);
135099
+ let captured = "";
135100
+ const { session } = await createResolvedAgentSession({
135101
+ sessionPurpose: "merger",
135102
+ pluginRunner: options.pluginRunner,
135103
+ cwd,
135104
+ systemPrompt: buildReviewSystemPrompt(),
135105
+ tools: "coding",
135106
+ onText: (delta) => {
135107
+ captured += delta;
135108
+ options.onAgentText?.(delta);
135109
+ },
135110
+ defaultProvider: model.provider,
135111
+ defaultModelId: model.modelId,
135112
+ fallbackProvider: settings.fallbackProvider,
135113
+ fallbackModelId: settings.fallbackModelId,
135114
+ defaultThinkingLevel: settings.defaultThinkingLevel,
135115
+ runAuditor: audit,
135116
+ settings,
135117
+ taskId
135118
+ });
135119
+ options.onSession?.(session);
135120
+ try {
135121
+ await withRateLimitRetry(async () => {
135122
+ await promptWithFallback(session, prompt);
135123
+ checkSessionError(session);
135124
+ }, { signal: options.signal });
135125
+ await accumulateSessionTokenUsage(store, taskId, session);
135126
+ } finally {
135127
+ session.dispose();
135128
+ }
135129
+ return captured;
135130
+ };
135131
+ }
135132
+ async function hasUnresolvedConflicts(cwd) {
135133
+ return (await git2(["ls-files", "-u"], cwd)).length > 0;
135134
+ }
135135
+ async function landSquash(input) {
135136
+ const { projectRootDir, mergeRoot, integrationBranch, tipSha, squashSha, taskId, audit, resolveConflicts: resolveConflicts2 } = input;
135137
+ const emit2 = (outcome, extra = {}) => audit.git({ type: "merge:ai-local-sync", target: integrationBranch, metadata: { taskId, outcome, squashSha, ...extra } }).catch(() => void 0);
135138
+ const currentBranch2 = await git2(["rev-parse", "--abbrev-ref", "HEAD"], projectRootDir).catch(() => "");
135139
+ if (currentBranch2 !== integrationBranch) {
135140
+ const adv = await advanceIntegrationBranchRef({
135141
+ rootDir: mergeRoot,
135142
+ projectRootDir,
135143
+ integrationBranch,
135144
+ newSha: squashSha,
135145
+ expectedCurrentSha: tipSha,
135146
+ taskId,
135147
+ audit
135148
+ });
135149
+ if (!adv.advanced) {
135150
+ if (adv.reason === "concurrent-advance" || adv.reason === "non-fast-forward-advance") {
135151
+ return { outcome: "concurrent", localSync: "skipped-other-branch" };
135152
+ }
135153
+ throw new Error(`AI merge could not advance ${integrationBranch} for ${taskId}: ${adv.reason} (${adv.diagnostic})`);
135154
+ }
135155
+ await emit2("skipped-other-branch", { currentBranch: currentBranch2 });
135156
+ return { outcome: "advanced", localSync: "skipped-other-branch" };
135157
+ }
135158
+ const head = await git2(["rev-parse", "HEAD"], projectRootDir).catch(() => "");
135159
+ if (head !== tipSha) {
135160
+ return { outcome: "concurrent", localSync: "skipped-other-branch" };
135161
+ }
135162
+ const dirty = (await git2(["status", "--porcelain"], projectRootDir)).length > 0;
135163
+ const stashed = dirty ? await gitOk(["stash", "push", "--include-untracked", "-m", `fusion-ai-merge-sync-${taskId}`], projectRootDir) : false;
135164
+ if (dirty && !stashed) {
135165
+ const adv = await advanceIntegrationBranchRef({
135166
+ rootDir: mergeRoot,
135167
+ projectRootDir,
135168
+ integrationBranch,
135169
+ newSha: squashSha,
135170
+ expectedCurrentSha: tipSha,
135171
+ taskId,
135172
+ audit
135173
+ });
135174
+ if (!adv.advanced) {
135175
+ if (adv.reason === "concurrent-advance" || adv.reason === "non-fast-forward-advance") {
135176
+ return { outcome: "concurrent", localSync: "skipped-dirty-unstashable" };
135177
+ }
135178
+ throw new Error(`AI merge could not advance ${integrationBranch} for ${taskId}: ${adv.reason} (${adv.diagnostic})`);
135179
+ }
135180
+ aiMergeLog.warn(`${taskId}: local checkout has un-stashable dirty state \u2014 advanced ${integrationBranch} without syncing your working tree; pull manually.`);
135181
+ await emit2("skipped-dirty-unstashable");
135182
+ return { outcome: "advanced", localSync: "skipped-dirty-unstashable" };
135183
+ }
135184
+ if (!await gitOk(["merge", "--ff-only", squashSha], projectRootDir)) {
135185
+ if (stashed) await gitOk(["stash", "pop"], projectRootDir);
135186
+ return { outcome: "concurrent", localSync: "skipped-other-branch" };
135187
+ }
135188
+ if (!stashed) {
135189
+ await emit2("ff");
135190
+ return { outcome: "advanced", localSync: "ff" };
135191
+ }
135192
+ if (await gitOk(["stash", "pop"], projectRootDir)) {
135193
+ await emit2("stash-ff-restore");
135194
+ return { outcome: "advanced", localSync: "stash-ff-restore" };
135195
+ }
135196
+ if (resolveConflicts2) {
135197
+ const conflicted = (await git2(["diff", "--name-only", "--diff-filter=U"], projectRootDir)).split("\n").map((l) => l.trim()).filter(Boolean);
135198
+ try {
135199
+ await resolveConflicts2(projectRootDir, buildStashResolvePrompt(conflicted));
135200
+ } catch (err) {
135201
+ aiMergeLog.warn(`${taskId}: AI stash-conflict resolver threw: ${err instanceof Error ? err.message : String(err)}`);
135202
+ }
135203
+ if (!await hasUnresolvedConflicts(projectRootDir)) {
135204
+ await gitOk(["reset"], projectRootDir);
135205
+ aiMergeLog.log(`${taskId}: reconciled your local edits with the new tip; original pre-merge edits also kept in a stash as a backup (\`git stash list\`).`);
135206
+ await emit2("stash-ff-airesolved", { conflicted, stashRetained: true });
135207
+ return { outcome: "advanced", localSync: "stash-ff-airesolved" };
135208
+ }
135209
+ }
135210
+ aiMergeLog.warn(`${taskId}: restoring your local changes onto the new tip conflicted and could not be auto-resolved. Your work is preserved in the stash (\`git stash list\`); re-apply with \`git stash pop\` and resolve manually.`);
135211
+ await emit2("stash-ff-conflict");
135212
+ return { outcome: "advanced", localSync: "stash-ff-conflict" };
135213
+ }
135214
+ function noOpResult(task, branch, reason) {
135215
+ return {
135216
+ task,
135217
+ branch,
135218
+ merged: false,
135219
+ noOp: true,
135220
+ ok: true,
135221
+ reason,
135222
+ worktreeRemoved: false,
135223
+ branchDeleted: false
135224
+ };
135225
+ }
135226
+ async function runAiMerge(store, projectRootDir, taskId, options = {}, deps = {}) {
135227
+ const task = await store.getTask(taskId);
135228
+ const branch = task.branch || canonicalFusionBranchName(taskId);
135229
+ if (task.column === "done" || task.column === "archived") {
135230
+ return noOpResult(task, branch, "already-finalized");
135231
+ }
135232
+ const blocker = getTaskMergeBlocker(task, { manual: options.manual === true });
135233
+ if (blocker) throw new Error(`Cannot merge ${taskId}: ${blocker}`);
135234
+ const settings = await store.getSettings();
135235
+ const projectDefaultBranch = await resolveIntegrationBranch(projectRootDir, settings);
135236
+ const mergeTarget = resolveTaskMergeTarget(task, { projectDefaultBranch });
135237
+ const integrationBranch = mergeTarget.branch;
135238
+ const audit = createRunAuditor(store, {
135239
+ runId: generateSyntheticRunId("ai-merge", taskId),
135240
+ agentId: "merger",
135241
+ taskId,
135242
+ phase: "merge"
135243
+ });
135244
+ const log25 = async (message) => {
135245
+ await store.logEntry(taskId, message, "AiMerge").catch(() => void 0);
135246
+ await store.appendAgentLog(taskId, message, "text", void 0, "merger").catch(() => void 0);
135247
+ };
135248
+ const setStatus = (status) => store.updateTask(taskId, { status }).catch(() => void 0);
135249
+ if (!await gitOk(["rev-parse", "--verify", `refs/heads/${branch}`], projectRootDir)) {
135250
+ const wasExecuted = !!task.baseCommitSha;
135251
+ const alreadyMerged = task.mergeDetails?.mergeConfirmed === true || !!task.mergeDetails?.commitSha;
135252
+ if (wasExecuted && !alreadyMerged) {
135253
+ await audit.git({
135254
+ type: "merge:ai-no-branch",
135255
+ target: branch,
135256
+ metadata: { taskId, kind: "executed-branch-missing", baseCommitSha: task.baseCommitSha }
135257
+ });
135258
+ throw new Error(
135259
+ `AI merge for ${taskId}: branch "${branch}" is missing, but the task was executed (baseCommitSha ${String(task.baseCommitSha).slice(0, 8)}) and has no recorded merge \u2014 its work appears lost. Not finalizing; investigate.`
135260
+ );
135261
+ }
135262
+ await audit.git({
135263
+ type: "merge:ai-no-branch",
135264
+ target: branch,
135265
+ metadata: { taskId, kind: alreadyMerged ? "already-merged" : "never-executed" }
135266
+ });
135267
+ return await finalizeTask(store, taskId, noOpResult(task, branch, alreadyMerged ? "already-merged" : "no-branch"));
135268
+ }
135269
+ if (!await gitOk(["rev-parse", "--verify", `refs/heads/${integrationBranch}`], projectRootDir)) {
135270
+ await audit.git({ type: "merge:ai-no-branch", target: integrationBranch, metadata: { taskId, kind: "integration-branch-missing" } });
135271
+ throw new Error(`AI merge for ${taskId}: target branch "${integrationBranch}" has no local ref (refs/heads/${integrationBranch}). Create or check out the branch locally before merging.`);
135272
+ }
135273
+ const maxPasses = Math.max(0, Math.trunc(settings.merger?.maxReviewPasses ?? 3));
135274
+ const mergeAgent = deps.mergeAgent ?? makeMutatingAgent(store, settings, taskId, options, audit, buildMergeSystemPrompt2(settings.agentPrompts));
135275
+ const reviewAgent = deps.reviewAgent ?? makeReviewAgent(store, settings, taskId, options, audit);
135276
+ const stashResolveAgent = deps.stashResolveAgent ?? makeMutatingAgent(store, settings, taskId, options, audit, buildStashResolveSystemPrompt());
135277
+ const includeTaskId = settings.includeTaskIdInCommit !== false;
135278
+ const trailers = taskTrailers(taskId, task.lineageId);
135279
+ const taskTitle = task.title?.trim() ? task.title.split("\n")[0] : void 0;
135280
+ await setStatus("merging");
135281
+ let advanceRetries = 0;
135282
+ while (true) {
135283
+ throwIfAborted2(options.signal, taskId);
135284
+ const tipSha = await git2(["rev-parse", "--verify", `refs/heads/${integrationBranch}`], projectRootDir);
135285
+ const mergeRoot = await mkdtemp2(join50(tmpdir5(), `fusion-ai-merge-${taskId.toLowerCase()}-`));
135286
+ let worktreeAdded = false;
135287
+ try {
135288
+ await git2(["worktree", "add", "--detach", mergeRoot, tipSha], projectRootDir);
135289
+ worktreeAdded = true;
135290
+ await audit.git({ type: "merge:ai-clean-room", target: integrationBranch, metadata: { taskId, tipSha, mergeRoot } });
135291
+ await log25(`AI merge: merging ${branch} into ${integrationBranch} (clean room at ${short(tipSha)})${advanceRetries ? ` \u2014 retry ${advanceRetries} after concurrent advance` : ""}`);
135292
+ const squashSha = await mergeAndReview({
135293
+ mergeRoot,
135294
+ branch,
135295
+ integrationBranch,
135296
+ tipSha,
135297
+ taskTitle,
135298
+ includeTaskId,
135299
+ trailers,
135300
+ taskId,
135301
+ maxPasses,
135302
+ mergeAgent,
135303
+ reviewAgent,
135304
+ audit,
135305
+ log: log25,
135306
+ setStatus,
135307
+ signal: options.signal
135308
+ });
135309
+ if (!squashSha) {
135310
+ await audit.git({ type: "merge:ai-empty", target: integrationBranch, metadata: { taskId, tipSha } });
135311
+ await log25(`AI merge: ${branch} had no net changes vs ${integrationBranch} \u2014 finalizing as no-op`);
135312
+ return await finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, tipSha, audit, log25, { empty: true });
135313
+ }
135314
+ await setStatus("landing");
135315
+ const landed = await landSquash({
135316
+ projectRootDir,
135317
+ mergeRoot,
135318
+ integrationBranch,
135319
+ tipSha,
135320
+ squashSha,
135321
+ taskId,
135322
+ audit,
135323
+ resolveConflicts: stashResolveAgent
135324
+ });
135325
+ if (landed.outcome === "concurrent") {
135326
+ if (advanceRetries < MAX_CONCURRENT_ADVANCE_RETRIES) {
135327
+ advanceRetries++;
135328
+ await log25(`AI merge: ${integrationBranch} moved during merge \u2014 rebuilding on new tip (retry ${advanceRetries})`);
135329
+ continue;
135330
+ }
135331
+ throw new Error(`AI merge could not advance ${integrationBranch} for ${taskId} after ${advanceRetries} retries (concurrent advances)`);
135332
+ }
135333
+ await log25(`AI merge: advanced ${integrationBranch} \u2192 ${short(squashSha)} (local checkout: ${landed.localSync})`);
135334
+ return await finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, squashSha, audit, log25, { empty: false });
135335
+ } finally {
135336
+ if (worktreeAdded) {
135337
+ await gitOk(["worktree", "remove", "--force", mergeRoot], projectRootDir);
135338
+ }
135339
+ await rm6(mergeRoot, { recursive: true, force: true }).catch(() => void 0);
135340
+ }
135341
+ }
135342
+ }
135343
+ async function mergeAndReview(input) {
135344
+ const { mergeRoot, branch, integrationBranch, tipSha, taskTitle, includeTaskId, trailers, taskId, maxPasses, mergeAgent, reviewAgent, audit, log: log25, setStatus, signal } = input;
135345
+ let priorReasons = [];
135346
+ for (let attempt = 0; ; attempt++) {
135347
+ throwIfAborted2(signal, taskId);
135348
+ await git2(["reset", "--hard", tipSha], mergeRoot);
135349
+ await git2(["clean", "-fd"], mergeRoot);
135350
+ if (attempt > 0) {
135351
+ await setStatus("merging");
135352
+ await log25(`AI merge: corrective re-merge (pass ${attempt}/${maxPasses}) addressing: ${priorReasons.join("; ")}`);
135353
+ }
135354
+ await mergeAgent(mergeRoot, buildMergePrompt2({
135355
+ taskId,
135356
+ branch,
135357
+ integrationBranch,
135358
+ tipSha,
135359
+ taskTitle,
135360
+ includeTaskId,
135361
+ trailers,
135362
+ correctiveReasons: priorReasons.length ? priorReasons : void 0
135363
+ }));
135364
+ let head = await git2(["rev-parse", "HEAD"], mergeRoot);
135365
+ if (head === tipSha) return null;
135366
+ await ensureCommitTaskMetadata(mergeRoot, taskId, includeTaskId, trailers);
135367
+ head = await git2(["rev-parse", "HEAD"], mergeRoot);
135368
+ await setStatus("reviewing");
135369
+ const diffStat = await git2(["diff", "--stat", `${tipSha}..${head}`], mergeRoot);
135370
+ const verdict = parseReviewVerdict(await reviewAgent(mergeRoot, buildReviewPrompt({
135371
+ taskId,
135372
+ branch,
135373
+ integrationBranch,
135374
+ tipSha,
135375
+ squashSha: head,
135376
+ diffStat,
135377
+ priorReasons
135378
+ })));
135379
+ await audit.git({
135380
+ type: "merge:ai-review-verdict",
135381
+ target: integrationBranch,
135382
+ metadata: { taskId, attempt, verdict: verdict.verdict, severity: verdict.severity, reasons: verdict.reasons, squashSha: head }
135383
+ });
135384
+ if (verdict.verdict === "approve") {
135385
+ await log25(`AI merge review (pass ${attempt + 1}): approved`);
135386
+ return head;
135387
+ }
135388
+ const budgetExhausted = attempt >= maxPasses;
135389
+ if (budgetExhausted) {
135390
+ if (verdict.severity === "blocking") {
135391
+ await audit.git({ type: "merge:ai-review-blocked", target: integrationBranch, metadata: { taskId, attempt, reasons: verdict.reasons } });
135392
+ await log25(`AI merge BLOCKED after ${attempt} corrective pass(es) \u2014 unresolved correctness concern: ${verdict.reasons.join("; ")}`);
135393
+ throw new AiMergeBlockedError(taskId, verdict.reasons);
135394
+ }
135395
+ await audit.git({ type: "merge:ai-review-landed-with-concerns", target: integrationBranch, metadata: { taskId, attempt, reasons: verdict.reasons, squashSha: head } });
135396
+ await log25(`AI merge: landing with unresolved advisory concern(s): ${verdict.reasons.join("; ")}`);
135397
+ return head;
135398
+ }
135399
+ priorReasons = verdict.reasons;
135400
+ await log25(`AI merge review (pass ${attempt + 1}): rejected (${verdict.severity}) \u2014 ${verdict.reasons.join("; ")}`);
135401
+ }
135402
+ }
135403
+ async function finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, landedSha, audit, log25, opts) {
135404
+ let branchDeleted = false;
135405
+ if (branch !== integrationBranch && await gitOk(["branch", "-D", branch], projectRootDir)) {
135406
+ branchDeleted = true;
135407
+ await audit.git({ type: "branch:delete", target: branch, metadata: { taskId, force: true } }).catch(() => void 0);
135408
+ }
135409
+ let worktreeRemoved = false;
135410
+ if (task.worktree) {
135411
+ worktreeRemoved = await gitOk(["worktree", "remove", "--force", task.worktree], projectRootDir);
135412
+ await store.updateTask(taskId, { worktree: null }).catch(() => void 0);
135413
+ }
135414
+ const result = {
135415
+ task,
135416
+ branch,
135417
+ merged: !opts.empty,
135418
+ noOp: opts.empty,
135419
+ ok: true,
135420
+ reason: opts.empty ? "no-net-changes" : void 0,
135421
+ commitSha: opts.empty ? void 0 : landedSha,
135422
+ mergeConfirmed: !opts.empty,
135423
+ worktreeRemoved,
135424
+ branchDeleted
135425
+ };
135426
+ await audit.git({ type: "merge:ai-landed", target: integrationBranch, metadata: { taskId, landedSha, empty: opts.empty } }).catch(() => void 0);
135427
+ await log25(opts.empty ? `AI merge: finalized ${taskId} (no-op) \u2192 done` : `AI merge: landed ${short(landedSha)}, task \u2192 done`);
135428
+ return await finalizeTask(store, taskId, result);
135429
+ }
135430
+ async function finalizeTask(store, taskId, result) {
135431
+ await store.updateTask(taskId, { status: null }).catch(() => void 0);
135432
+ const task = await store.moveTask(taskId, "done");
135433
+ result.task = task;
135434
+ store.emit("task:merged", result);
135435
+ return result;
135436
+ }
135437
+ function throwIfAborted2(signal, taskId) {
135438
+ if (signal?.aborted) {
135439
+ const err = new Error(`AI merge aborted for ${taskId}`);
135440
+ err.name = "MergeAbortedError";
135441
+ throw err;
135442
+ }
135443
+ }
135444
+ var execFileAsync10, aiMergeLog, MAX_CONCURRENT_ADVANCE_RETRIES, FUSION_TASK_ID_TRAILER_KEY3, REVIEW_VERDICT_MARKER, VERDICT_LINE_RE, SEVERITY_LINE_RE, AiMergeBlockedError;
135445
+ var init_merger_ai = __esm({
135446
+ "../engine/src/merger-ai.ts"() {
135447
+ "use strict";
135448
+ init_src();
135449
+ init_worktree_names();
135450
+ init_integration_branch();
135451
+ init_merger_ref_update_advance();
135452
+ init_agent_session_helpers();
135453
+ init_pi();
135454
+ init_rate_limit_retry();
135455
+ init_usage_limit_detector();
135456
+ init_session_token_usage();
135457
+ init_run_audit();
135458
+ init_logger2();
135459
+ execFileAsync10 = promisify43(execFile15);
135460
+ aiMergeLog = createLogger2("merger-ai");
135461
+ MAX_CONCURRENT_ADVANCE_RETRIES = 3;
135462
+ FUSION_TASK_ID_TRAILER_KEY3 = "Fusion-Task-Id";
135463
+ REVIEW_VERDICT_MARKER = "REVIEW_VERDICT:";
135464
+ VERDICT_LINE_RE = /REVIEW_VERDICT:\s*(approve|reject)\b/i;
135465
+ SEVERITY_LINE_RE = /SEVERITY:\s*(blocking|advisory)\b/i;
135466
+ AiMergeBlockedError = class extends Error {
135467
+ taskId;
135468
+ reasons;
135469
+ constructor(taskId, reasons) {
135470
+ super(`AI merge blocked ${taskId} (unresolved correctness concern): ${reasons.join("; ") || "no reason given"}`);
135471
+ this.name = "AiMergeBlockedError";
135472
+ this.taskId = taskId;
135473
+ this.reasons = reasons;
135474
+ }
135475
+ };
135476
+ }
135477
+ });
135478
+
134176
135479
  // ../engine/src/research-dispatcher.ts
134177
135480
  var log24, ResearchRunDispatcher;
134178
135481
  var init_research_dispatcher = __esm({
@@ -134415,9 +135718,9 @@ var init_provider_adapters = __esm({
134415
135718
  });
134416
135719
 
134417
135720
  // ../engine/src/remote-access/tunnel-process-manager.ts
134418
- import { EventEmitter as EventEmitter24 } from "node:events";
134419
- import { exec as exec36, execFile as execFile15, spawn as spawn8 } from "node:child_process";
134420
- import { promisify as promisify43 } from "node:util";
135721
+ import { EventEmitter as EventEmitter25 } from "node:events";
135722
+ import { exec as exec36, execFile as execFile16, spawn as spawn8 } from "node:child_process";
135723
+ import { promisify as promisify44 } from "node:util";
134421
135724
  function nowIso2() {
134422
135725
  return (/* @__PURE__ */ new Date()).toISOString();
134423
135726
  }
@@ -134457,7 +135760,7 @@ function toStateError(code, err) {
134457
135760
  at: nowIso2()
134458
135761
  };
134459
135762
  }
134460
- var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync10, execAsync32, LineBuffer, TunnelProcessManager;
135763
+ var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync11, execAsync32, LineBuffer, TunnelProcessManager;
134461
135764
  var init_tunnel_process_manager = __esm({
134462
135765
  "../engine/src/remote-access/tunnel-process-manager.ts"() {
134463
135766
  "use strict";
@@ -134466,8 +135769,8 @@ var init_tunnel_process_manager = __esm({
134466
135769
  init_provider_adapters();
134467
135770
  DEFAULT_MAX_LOG_ENTRIES = 400;
134468
135771
  DEFAULT_STOP_TIMEOUT_MS2 = 5e3;
134469
- execFileAsync10 = promisify43(execFile15);
134470
- execAsync32 = promisify43(exec36);
135772
+ execFileAsync11 = promisify44(execFile16);
135773
+ execAsync32 = promisify44(exec36);
134471
135774
  LineBuffer = class {
134472
135775
  pending = "";
134473
135776
  push(chunk) {
@@ -134482,7 +135785,7 @@ var init_tunnel_process_manager = __esm({
134482
135785
  return tail ? [tail] : [];
134483
135786
  }
134484
135787
  };
134485
- TunnelProcessManager = class extends EventEmitter24 {
135788
+ TunnelProcessManager = class extends EventEmitter25 {
134486
135789
  maxLogEntries;
134487
135790
  defaultStopTimeoutMs;
134488
135791
  spawnImpl;
@@ -134544,7 +135847,7 @@ var init_tunnel_process_manager = __esm({
134544
135847
  return null;
134545
135848
  }
134546
135849
  try {
134547
- const { stdout } = await execFileAsync10("tailscale", ["status", "--json"], { timeout: 3e3 });
135850
+ const { stdout } = await execFileAsync11("tailscale", ["status", "--json"], { timeout: 3e3 });
134548
135851
  const data = JSON.parse(String(stdout));
134549
135852
  const dnsName = data.Self?.DNSName?.replace(/\.$/, "");
134550
135853
  if (!dnsName) {
@@ -134567,7 +135870,7 @@ var init_tunnel_process_manager = __esm({
134567
135870
  ];
134568
135871
  for (const resetCommand of resetCommands) {
134569
135872
  try {
134570
- await execFileAsync10(resetCommand.command, resetCommand.args, { timeout: 5e3 });
135873
+ await execFileAsync11(resetCommand.command, resetCommand.args, { timeout: 5e3 });
134571
135874
  return;
134572
135875
  } catch {
134573
135876
  }
@@ -134868,8 +136171,8 @@ var init_tunnel_process_manager = __esm({
134868
136171
  });
134869
136172
 
134870
136173
  // ../engine/src/project-engine.ts
134871
- import { execFile as execFile16 } from "node:child_process";
134872
- import { promisify as promisify44 } from "node:util";
136174
+ import { execFile as execFile17 } from "node:child_process";
136175
+ import { promisify as promisify45 } from "node:util";
134873
136176
  function formatErrorDetails(error) {
134874
136177
  if (error instanceof Error) {
134875
136178
  return {
@@ -134884,6 +136187,40 @@ function isInvalidDoneTransitionError(error) {
134884
136187
  const message = error instanceof Error ? error.message : String(error);
134885
136188
  return message.includes("Invalid transition:") && message.includes("\u2192 'done'");
134886
136189
  }
136190
+ async function verifyMergeConfirmedReachability(args) {
136191
+ const { commitSha, integrationBranch, cwd } = args;
136192
+ if (!commitSha || !commitSha.trim()) {
136193
+ return { reachable: true, skipped: "no-commit-sha" };
136194
+ }
136195
+ if (!integrationBranch || !integrationBranch.trim()) {
136196
+ return { reachable: true, skipped: "no-integration-branch" };
136197
+ }
136198
+ try {
136199
+ await execFileAsync12("git", ["cat-file", "-e", `${commitSha}^{commit}`], {
136200
+ cwd,
136201
+ timeout: 1e4
136202
+ });
136203
+ } catch (error) {
136204
+ const diagnostic = error instanceof Error ? error.message : String(error);
136205
+ return { reachable: false, reason: "commit-missing", diagnostic };
136206
+ }
136207
+ try {
136208
+ await execFileAsync12(
136209
+ "git",
136210
+ ["merge-base", "--is-ancestor", commitSha, `refs/heads/${integrationBranch}`],
136211
+ { cwd, timeout: 1e4 }
136212
+ );
136213
+ return { reachable: true };
136214
+ } catch (error) {
136215
+ const err = error;
136216
+ const code = typeof err.code === "number" ? err.code : void 0;
136217
+ const diagnostic = err.message ?? String(error);
136218
+ if (code === 1) {
136219
+ return { reachable: false, reason: "not-ancestor", diagnostic };
136220
+ }
136221
+ return { reachable: false, reason: "git-error", diagnostic };
136222
+ }
136223
+ }
134887
136224
  function buildVerificationFailureSignature(error) {
134888
136225
  const commandResult = error.verificationResult.testResult ?? error.verificationResult.buildResult;
134889
136226
  const lane = commandResult?.command?.trim() || error.verificationResult.failedCommand?.trim() || "verification-failure";
@@ -134894,7 +136231,7 @@ function buildVerificationFailureSignature(error) {
134894
136231
  failedCommand: commandResult?.command ?? error.verificationResult.failedCommand ?? null
134895
136232
  }).signature;
134896
136233
  }
134897
- var execFileAsync11, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
136234
+ var execFileAsync12, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
134898
136235
  var init_project_engine = __esm({
134899
136236
  "../engine/src/project-engine.ts"() {
134900
136237
  "use strict";
@@ -134908,6 +136245,7 @@ var init_project_engine = __esm({
134908
136245
  init_auth_storage();
134909
136246
  init_cron_runner();
134910
136247
  init_merger();
136248
+ init_merger_ai();
134911
136249
  init_concurrency();
134912
136250
  init_logger2();
134913
136251
  init_research_orchestrator();
@@ -134916,7 +136254,7 @@ var init_project_engine = __esm({
134916
136254
  init_run_audit();
134917
136255
  init_verification_followup_dedup();
134918
136256
  init_tunnel_process_manager();
134919
- execFileAsync11 = promisify44(execFile16);
136257
+ execFileAsync12 = promisify45(execFile17);
134920
136258
  MERGE_HANDOFF_GRACE_MS = 300;
134921
136259
  isRemoteActive = (ra) => ra?.activeProvider != null && (ra.providers[ra.activeProvider]?.enabled ?? false);
134922
136260
  ProjectEngine = class _ProjectEngine {
@@ -135661,7 +136999,7 @@ ${detail}`
135661
136999
  async checkExecutableAvailable(command) {
135662
137000
  const checker = process.platform === "win32" ? "where" : "which";
135663
137001
  try {
135664
- await execFileAsync11(checker, [command]);
137002
+ await execFileAsync12(checker, [command]);
135665
137003
  return { available: true };
135666
137004
  } catch {
135667
137005
  return {
@@ -135861,6 +137199,112 @@ ${detail}`
135861
137199
  continue;
135862
137200
  }
135863
137201
  if (task.mergeDetails?.mergeConfirmed) {
137202
+ const integrationBranchForGate = task.mergeDetails.mergeTargetBranch || task.baseBranch || "main";
137203
+ const reachability = await verifyMergeConfirmedReachability({
137204
+ commitSha: task.mergeDetails.commitSha,
137205
+ integrationBranch: integrationBranchForGate,
137206
+ cwd
137207
+ });
137208
+ if (!reachability.reachable) {
137209
+ const sha = task.mergeDetails.commitSha || "";
137210
+ const shortSha = sha ? sha.slice(0, 8) : "<no-sha>";
137211
+ const currentRetries = task.mergeRetries ?? 0;
137212
+ const budgetExhausted = currentRetries >= _ProjectEngine.MAX_AUTO_MERGE_RETRIES;
137213
+ const cleanedMergeDetails = {
137214
+ ...task.mergeDetails,
137215
+ mergeConfirmed: false,
137216
+ commitSha: void 0,
137217
+ mergedAt: void 0,
137218
+ landedFiles: void 0,
137219
+ filesChanged: void 0,
137220
+ insertions: void 0,
137221
+ deletions: void 0,
137222
+ noOpVerifiedShortCircuit: void 0,
137223
+ landedFilesAttributionRestricted: void 0
137224
+ };
137225
+ if (budgetExhausted) {
137226
+ const errorMsg = `Auto-merge fast-path refused after ${currentRetries} attempts: commit ${shortSha} is not reachable from ${integrationBranchForGate} (${reachability.reason}). Manual review required.`;
137227
+ runtimeLog.warn(
137228
+ `Auto-merge: ${taskId} fast-path REFUSED + budget exhausted \u2014 ${reachability.reason}: ${reachability.diagnostic}`
137229
+ );
137230
+ await store.logEntry(
137231
+ taskId,
137232
+ `[FN-5627] Auto-merge fast-path refused (retry budget exhausted) \u2014 ${errorMsg}`
137233
+ );
137234
+ await store.updateTask(taskId, {
137235
+ mergeDetails: cleanedMergeDetails,
137236
+ status: "failed",
137237
+ error: errorMsg
137238
+ });
137239
+ try {
137240
+ const auditor = createRunAuditor(store, {
137241
+ runId: generateSyntheticRunId("merger-fast-path-refused", taskId),
137242
+ agentId: "merger",
137243
+ taskId,
137244
+ phase: "auto-merge-fast-path-gate"
137245
+ });
137246
+ await auditor.database({
137247
+ type: "merger:fast-path-blocked-foreign-commit",
137248
+ target: taskId,
137249
+ metadata: {
137250
+ taskId,
137251
+ commitSha: sha,
137252
+ integrationBranch: integrationBranchForGate,
137253
+ reason: reachability.reason,
137254
+ diagnostic: reachability.diagnostic,
137255
+ mergeRetries: currentRetries,
137256
+ budgetExhausted: true
137257
+ }
137258
+ });
137259
+ } catch (auditErr) {
137260
+ runtimeLog.warn(
137261
+ `Auto-merge: ${taskId} fast-path audit emit failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
137262
+ );
137263
+ }
137264
+ continue;
137265
+ }
137266
+ const nextRetries = currentRetries + 1;
137267
+ runtimeLog.warn(
137268
+ `Auto-merge: ${taskId} fast-path REFUSED \u2014 auto-recovering (attempt ${nextRetries}/${_ProjectEngine.MAX_AUTO_MERGE_RETRIES}): ${reachability.reason}: ${reachability.diagnostic}`
137269
+ );
137270
+ await store.logEntry(
137271
+ taskId,
137272
+ `Auto-recovered: fast-path refused \u2014 cleared poisoned mergeDetails (commit ${shortSha} not reachable from ${integrationBranchForGate}, ${reachability.reason}). Re-enqueueing for fresh merge attempt ${nextRetries}/${_ProjectEngine.MAX_AUTO_MERGE_RETRIES} [FN-5627].`
137273
+ );
137274
+ await store.updateTask(taskId, {
137275
+ mergeDetails: cleanedMergeDetails,
137276
+ mergeRetries: nextRetries,
137277
+ status: null,
137278
+ error: null
137279
+ });
137280
+ try {
137281
+ const auditor = createRunAuditor(store, {
137282
+ runId: generateSyntheticRunId("merger-fast-path-auto-recovered", taskId),
137283
+ agentId: "merger",
137284
+ taskId,
137285
+ phase: "auto-merge-fast-path-gate"
137286
+ });
137287
+ await auditor.database({
137288
+ type: "merger:fast-path-auto-recovered",
137289
+ target: taskId,
137290
+ metadata: {
137291
+ taskId,
137292
+ commitSha: sha,
137293
+ integrationBranch: integrationBranchForGate,
137294
+ reason: reachability.reason,
137295
+ diagnostic: reachability.diagnostic,
137296
+ mergeRetries: nextRetries,
137297
+ maxRetries: _ProjectEngine.MAX_AUTO_MERGE_RETRIES
137298
+ }
137299
+ });
137300
+ } catch (auditErr) {
137301
+ runtimeLog.warn(
137302
+ `Auto-merge: ${taskId} fast-path audit emit failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
137303
+ );
137304
+ }
137305
+ this.internalEnqueueMerge(taskId);
137306
+ continue;
137307
+ }
135864
137308
  const blockerReason = getTaskHardMergeBlocker(task);
135865
137309
  if (blockerReason) {
135866
137310
  await store.updateTask(taskId, {
@@ -135996,10 +137440,10 @@ ${detail}`
135996
137440
  const pool = this.runtime.worktreePool;
135997
137441
  const agentStore = this.runtime.agentStore;
135998
137442
  const usageLimitPauser = this.runtime.usageLimitPauser;
135999
- const rawMerge = () => {
137443
+ const rawMerge = async () => {
136000
137444
  this.activeMergeTaskId = taskId;
136001
137445
  this.mergeAbortController = new AbortController();
136002
- return aiMergeTask(store, cwd, taskId, {
137446
+ const mergerOptions = {
136003
137447
  manual: !!manualResolver,
136004
137448
  pool,
136005
137449
  usageLimitPauser,
@@ -136008,7 +137452,9 @@ ${detail}`
136008
137452
  onSession: (session) => {
136009
137453
  this.activeMergeSession = session;
136010
137454
  }
136011
- });
137455
+ };
137456
+ const mergerMode = normalizeMergerMode((await store.getSettings().catch(() => ({}))).merger?.mode);
137457
+ return mergerMode === "ai" ? runAiMerge(store, cwd, taskId, mergerOptions) : aiMergeTask(store, cwd, taskId, mergerOptions);
136012
137458
  };
136013
137459
  let result;
136014
137460
  if (semaphore) {
@@ -138564,8 +140010,8 @@ var require_proper_lockfile = __commonJS({
138564
140010
  import { mkdirSync as mkdirSync10 } from "node:fs";
138565
140011
  import { access as access7, unlink as unlink7, writeFile as writeFile18 } from "node:fs/promises";
138566
140012
  import net from "node:net";
138567
- import { platform as platform3, tmpdir as tmpdir5 } from "node:os";
138568
- import { join as join50 } from "node:path";
140013
+ import { platform as platform3, tmpdir as tmpdir6 } from "node:os";
140014
+ import { join as join51 } from "node:path";
138569
140015
  import { createHash as createHash14 } from "node:crypto";
138570
140016
  function projectHash(projectId) {
138571
140017
  return createHash14("sha1").update(projectId).digest("hex").slice(0, 16);
@@ -138575,10 +140021,10 @@ function computeEngineSocketPath(projectId) {
138575
140021
  if (platform3() === "win32") {
138576
140022
  return `\\\\.\\pipe\\fusion-engine-${hash}`;
138577
140023
  }
138578
- return join50(tmpdir5(), `fusion-engine-${hash}.sock`);
140024
+ return join51(tmpdir6(), `fusion-engine-${hash}.sock`);
138579
140025
  }
138580
140026
  function computeEngineLockFilePath(workingDir) {
138581
- return join50(workingDir, ".fusion", "engine.lock");
140027
+ return join51(workingDir, ".fusion", "engine.lock");
138582
140028
  }
138583
140029
  async function ensureLockTargetExists(lockPath) {
138584
140030
  try {
@@ -138588,7 +140034,7 @@ async function ensureLockTargetExists(lockPath) {
138588
140034
  }
138589
140035
  }
138590
140036
  async function acquireLockfile(workingDir, onCompromised) {
138591
- const dir = join50(workingDir, ".fusion");
140037
+ const dir = join51(workingDir, ".fusion");
138592
140038
  mkdirSync10(dir, { recursive: true });
138593
140039
  const path6 = computeEngineLockFilePath(workingDir);
138594
140040
  await ensureLockTargetExists(path6);
@@ -139204,7 +140650,7 @@ var init_node_health_monitor = __esm({
139204
140650
  });
139205
140651
 
139206
140652
  // ../engine/src/hybrid-executor.ts
139207
- import { EventEmitter as EventEmitter25 } from "node:events";
140653
+ import { EventEmitter as EventEmitter26 } from "node:events";
139208
140654
  var HybridExecutor;
139209
140655
  var init_hybrid_executor = __esm({
139210
140656
  "../engine/src/hybrid-executor.ts"() {
@@ -139212,7 +140658,7 @@ var init_hybrid_executor = __esm({
139212
140658
  init_project_manager();
139213
140659
  init_node_health_monitor();
139214
140660
  init_logger2();
139215
- HybridExecutor = class extends EventEmitter25 {
140661
+ HybridExecutor = class extends EventEmitter26 {
139216
140662
  /**
139217
140663
  * @param centralCore - CentralCore reference for global coordination
139218
140664
  * @param options - Optional configuration callbacks
@@ -139980,6 +141426,7 @@ __export(src_exports2, {
139980
141426
  BranchConflictError: () => BranchConflictError,
139981
141427
  BranchCrossContaminationError: () => BranchCrossContaminationError,
139982
141428
  CronRunner: () => CronRunner,
141429
+ DEFAULT_GOAL_INJECTION_CHAR_BUDGET: () => DEFAULT_GOAL_INJECTION_CHAR_BUDGET,
139983
141430
  DEFAULT_NTFY_EVENTS: () => DEFAULT_NTFY_EVENTS,
139984
141431
  DiffVolumeRegressionError: () => DiffVolumeRegressionError,
139985
141432
  EngineAlreadyRunningError: () => EngineAlreadyRunningError,
@@ -140004,6 +141451,7 @@ __export(src_exports2, {
140004
141451
  KNOWN_EXTERNAL_INTEGRATIONS: () => KNOWN_EXTERNAL_INTEGRATIONS,
140005
141452
  LLMSynthesisProvider: () => LLMSynthesisProvider,
140006
141453
  LocalDocsProvider: () => LocalDocsProvider,
141454
+ MAX_INJECTED_GOALS: () => MAX_INJECTED_GOALS,
140007
141455
  MOCK_PROVIDER_ID: () => MOCK_PROVIDER_ID3,
140008
141456
  MOCK_SYNTHETIC_TOKEN_USAGE: () => MOCK_SYNTHETIC_TOKEN_USAGE,
140009
141457
  MergeAbortedError: () => MergeAbortedError,
@@ -140079,6 +141527,7 @@ __export(src_exports2, {
140079
141527
  assertSafeUrl: () => assertSafeUrl,
140080
141528
  auditSquashMerge: () => auditSquashMerge,
140081
141529
  buildAgentChatPrompt: () => buildAgentChatPrompt,
141530
+ buildGoalContextSection: () => buildGoalContextSection,
140082
141531
  buildNtfyClickUrl: () => buildNtfyClickUrl,
140083
141532
  buildRuntimeResolutionContext: () => buildRuntimeResolutionContext,
140084
141533
  buildSystemPromptWithInstructions: () => buildSystemPromptWithInstructions,
@@ -140221,6 +141670,7 @@ var init_src2 = __esm({
140221
141670
  init_external_integrations();
140222
141671
  init_web_fetch();
140223
141672
  init_error_classifier();
141673
+ init_goal_context_injector();
140224
141674
  init_worktrunk_installer();
140225
141675
  init_worktrunk_failure_handler();
140226
141676
  init_usage_limit_detector();
@@ -140522,7 +141972,7 @@ var init_planning_board_tools = __esm({
140522
141972
 
140523
141973
  // ../dashboard/src/planning.ts
140524
141974
  import { randomUUID as randomUUID22 } from "node:crypto";
140525
- import { EventEmitter as EventEmitter26 } from "node:events";
141975
+ import { EventEmitter as EventEmitter27 } from "node:events";
140526
141976
  function ensureEngineReady() {
140527
141977
  return Promise.resolve();
140528
141978
  }
@@ -141546,7 +142996,7 @@ For completion:
141546
142996
  process.on("beforeExit", () => {
141547
142997
  clearInterval(cleanupInterval2);
141548
142998
  });
141549
- PlanningStreamManager = class extends EventEmitter26 {
142999
+ PlanningStreamManager = class extends EventEmitter27 {
141550
143000
  constructor(bufferSize = 100) {
141551
143001
  super();
141552
143002
  this.bufferSize = bufferSize;
@@ -141761,7 +143211,7 @@ var init_ai_session_timeout = __esm({
141761
143211
  });
141762
143212
 
141763
143213
  // ../dashboard/src/subtask-breakdown.ts
141764
- import { EventEmitter as EventEmitter27 } from "node:events";
143214
+ import { EventEmitter as EventEmitter28 } from "node:events";
141765
143215
  function cleanupInMemorySubtaskSession(sessionId) {
141766
143216
  const session = sessions2.get(sessionId);
141767
143217
  if (!session) {
@@ -141803,7 +143253,7 @@ var init_subtask_breakdown = __esm({
141803
143253
  process.on("beforeExit", () => {
141804
143254
  clearInterval(cleanupInterval3);
141805
143255
  });
141806
- SubtaskStreamManager = class extends EventEmitter27 {
143256
+ SubtaskStreamManager = class extends EventEmitter28 {
141807
143257
  constructor(bufferSize = 100) {
141808
143258
  super();
141809
143259
  this.bufferSize = bufferSize;
@@ -141865,7 +143315,7 @@ var init_subtask_breakdown = __esm({
141865
143315
  });
141866
143316
 
141867
143317
  // ../dashboard/src/mission-interview.ts
141868
- import { EventEmitter as EventEmitter28 } from "node:events";
143318
+ import { EventEmitter as EventEmitter29 } from "node:events";
141869
143319
  function cleanupInMemoryMissionSession(sessionId) {
141870
143320
  const session = sessions3.get(sessionId);
141871
143321
  if (!session) {
@@ -141916,7 +143366,7 @@ var init_mission_interview = __esm({
141916
143366
  cleanupInterval4 = setInterval(cleanupExpiredSessions3, CLEANUP_INTERVAL_MS4);
141917
143367
  cleanupInterval4.unref?.();
141918
143368
  process.on("beforeExit", () => clearInterval(cleanupInterval4));
141919
- MissionInterviewStreamManager = class extends EventEmitter28 {
143369
+ MissionInterviewStreamManager = class extends EventEmitter29 {
141920
143370
  constructor(bufferSize = 100) {
141921
143371
  super();
141922
143372
  this.bufferSize = bufferSize;
@@ -141984,7 +143434,7 @@ var init_mission_interview = __esm({
141984
143434
  });
141985
143435
 
141986
143436
  // ../dashboard/src/milestone-slice-interview.ts
141987
- import { EventEmitter as EventEmitter29 } from "node:events";
143437
+ import { EventEmitter as EventEmitter30 } from "node:events";
141988
143438
  function cleanupInMemorySession2(sessionId) {
141989
143439
  const session = sessions4.get(sessionId);
141990
143440
  if (!session) {
@@ -142036,7 +143486,7 @@ var init_milestone_slice_interview = __esm({
142036
143486
  cleanupInterval5 = setInterval(cleanupExpiredSessions4, CLEANUP_INTERVAL_MS5);
142037
143487
  cleanupInterval5.unref?.();
142038
143488
  process.on("beforeExit", () => clearInterval(cleanupInterval5));
142039
- MilestoneSliceInterviewStreamManager = class extends EventEmitter29 {
143489
+ MilestoneSliceInterviewStreamManager = class extends EventEmitter30 {
142040
143490
  constructor(bufferSize = 100) {
142041
143491
  super();
142042
143492
  this.bufferSize = bufferSize;
@@ -142351,13 +143801,13 @@ var init_cli_spawn = __esm({
142351
143801
  // ../../plugins/fusion-plugin-hermes-runtime/dist/fusion-skill-install.js
142352
143802
  import { cpSync, existsSync as existsSync43, lstatSync as lstatSync2, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync as unlinkSync2 } from "node:fs";
142353
143803
  import { homedir as homedir8 } from "node:os";
142354
- import { basename as basename14, dirname as dirname19, join as join51, resolve as resolve29 } from "node:path";
143804
+ import { basename as basename14, dirname as dirname19, join as join52, resolve as resolve29 } from "node:path";
142355
143805
  import { fileURLToPath as fileURLToPath4 } from "node:url";
142356
143806
  function resolveHermesHome(profile) {
142357
- const base = process.env.HERMES_HOME ?? join51(homedir8(), ".hermes");
143807
+ const base = process.env.HERMES_HOME ?? join52(homedir8(), ".hermes");
142358
143808
  if (!profile || profile === "default")
142359
143809
  return base;
142360
- return join51(base, "profiles", profile);
143810
+ return join52(base, "profiles", profile);
142361
143811
  }
142362
143812
  function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
142363
143813
  const here = fileURLToPath4(moduleUrl);
@@ -142372,14 +143822,14 @@ function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
142372
143822
  function resolveBundledFusionSkillSource() {
142373
143823
  const candidates = getFusionSkillSourceCandidates();
142374
143824
  for (const candidate of candidates) {
142375
- if (existsSync43(join51(candidate, "SKILL.md")))
143825
+ if (existsSync43(join52(candidate, "SKILL.md")))
142376
143826
  return candidate;
142377
143827
  }
142378
143828
  return null;
142379
143829
  }
142380
143830
  function installFusionSkillIntoHermesHome(options = {}) {
142381
143831
  const sourceDir = options.sourceDir ?? resolveBundledFusionSkillSource();
142382
- const targetDir = join51(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
143832
+ const targetDir = join52(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
142383
143833
  if (!sourceDir) {
142384
143834
  return {
142385
143835
  outcome: "warning",
@@ -142468,7 +143918,7 @@ function isBrokenSymlink(path6) {
142468
143918
  }
142469
143919
  }
142470
143920
  function looksLikePriorFusionInstall(path6) {
142471
- const skillMd = join51(path6, "SKILL.md");
143921
+ const skillMd = join52(path6, "SKILL.md");
142472
143922
  if (!existsSync43(skillMd))
142473
143923
  return false;
142474
143924
  try {
@@ -142483,7 +143933,7 @@ function looksLikeFusionSkillTarget(path6) {
142483
143933
  return false;
142484
143934
  if (basename14(path6).toLowerCase() === FUSION_SKILL_NAME)
142485
143935
  return true;
142486
- return existsSync43(join51(path6, "SKILL.md"));
143936
+ return existsSync43(join52(path6, "SKILL.md"));
142487
143937
  }
142488
143938
  var FUSION_SKILL_NAME;
142489
143939
  var init_fusion_skill_install = __esm({
@@ -142861,8 +144311,8 @@ var init_pi_module = __esm({
142861
144311
 
142862
144312
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/mcp-config.js
142863
144313
  import { writeFileSync as writeFileSync5 } from "node:fs";
142864
- import { tmpdir as tmpdir6 } from "node:os";
142865
- import { join as join52 } from "node:path";
144314
+ import { tmpdir as tmpdir7 } from "node:os";
144315
+ import { join as join53 } from "node:path";
142866
144316
  import { fileURLToPath as fileURLToPath5 } from "node:url";
142867
144317
  import { dirname as dirname20 } from "node:path";
142868
144318
  function toolsToMcpToolDefs(tools) {
@@ -142876,16 +144326,16 @@ function toolsToMcpToolDefs(tools) {
142876
144326
  }
142877
144327
  function writeOpenClawMcpBridgeFiles(toolDefs, cacheKey) {
142878
144328
  const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
142879
- const schemaPath = join52(tmpdir6(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
144329
+ const schemaPath = join53(tmpdir7(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
142880
144330
  writeFileSync5(schemaPath, JSON.stringify(toolDefs));
142881
144331
  const __filename = fileURLToPath5(import.meta.url);
142882
144332
  const __dirname2 = dirname20(__filename);
142883
- const serverPath = join52(__dirname2, "mcp-schema-server.cjs");
144333
+ const serverPath = join53(__dirname2, "mcp-schema-server.cjs");
142884
144334
  const serverConfig = {
142885
144335
  command: "node",
142886
144336
  args: [serverPath, schemaPath]
142887
144337
  };
142888
- const serverConfigPath = join52(tmpdir6(), `openclaw-runtime-mcp-server-${suffix}.json`);
144338
+ const serverConfigPath = join53(tmpdir7(), `openclaw-runtime-mcp-server-${suffix}.json`);
142889
144339
  writeFileSync5(serverConfigPath, JSON.stringify(serverConfig));
142890
144340
  return {
142891
144341
  schemaPath,
@@ -143130,10 +144580,10 @@ var init_context = __esm({
143130
144580
 
143131
144581
  // ../dashboard/src/github.ts
143132
144582
  import { exec as exec37 } from "node:child_process";
143133
- import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
143134
- import { tmpdir as tmpdir7 } from "node:os";
143135
- import { join as join53 } from "node:path";
143136
- import { promisify as promisify45 } from "node:util";
144583
+ import { mkdtemp as mkdtemp3, rm as rm7 } from "node:fs/promises";
144584
+ import { tmpdir as tmpdir8 } from "node:os";
144585
+ import { join as join54 } from "node:path";
144586
+ import { promisify as promisify46 } from "node:util";
143137
144587
  function quoteGitArg(value) {
143138
144588
  return `'${value.replaceAll("'", "'\\''")}'`;
143139
144589
  }
@@ -143391,7 +144841,7 @@ var init_github = __esm({
143391
144841
  "../dashboard/src/github.ts"() {
143392
144842
  "use strict";
143393
144843
  init_src();
143394
- execAsync33 = promisify45(exec37);
144844
+ execAsync33 = promisify46(exec37);
143395
144845
  PR_REVIEW_PAGE_SIZE = 100;
143396
144846
  MAX_PR_REVIEW_PAGES = 10;
143397
144847
  MAX_BADGE_BATCH_SIZE = 100;
@@ -144059,8 +145509,8 @@ var init_github = __esm({
144059
145509
  { timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
144060
145510
  );
144061
145511
  const mergeBase = mergeBaseStdout.trim();
144062
- const indexDir = await mkdtemp2(join53(tmpdir7(), "fn-pr-conflict-"));
144063
- const indexPath = join53(indexDir, "index");
145512
+ const indexDir = await mkdtemp3(join54(tmpdir8(), "fn-pr-conflict-"));
145513
+ const indexPath = join54(indexDir, "index");
144064
145514
  const gitEnv = { ...process.env, GIT_INDEX_FILE: indexPath };
144065
145515
  try {
144066
145516
  await execAsync33(`git -C ${quoteGitArg(opts.repoRoot)} read-tree -m ${quoteGitArg(mergeBase)} ${quoteGitArg(baseRef)} ${quoteGitArg(headRef)}`, {
@@ -144079,7 +145529,7 @@ var init_github = __esm({
144079
145529
  )
144080
145530
  ];
144081
145531
  } finally {
144082
- await rm6(indexDir, { recursive: true, force: true });
145532
+ await rm7(indexDir, { recursive: true, force: true });
144083
145533
  }
144084
145534
  } catch {
144085
145535
  conflictingFiles = [];
@@ -146111,8 +147561,18 @@ var init_github_tracking_reconciler = __esm({
146111
147561
  }
146112
147562
  });
146113
147563
 
147564
+ // ../dashboard/src/github-source-issue-close.ts
147565
+ var init_github_source_issue_close = __esm({
147566
+ "../dashboard/src/github-source-issue-close.ts"() {
147567
+ "use strict";
147568
+ init_github_auth();
147569
+ init_github();
147570
+ init_github_tracking_state();
147571
+ }
147572
+ });
147573
+
146114
147574
  // ../dashboard/src/github-poll.ts
146115
- import { EventEmitter as EventEmitter30 } from "node:events";
147575
+ import { EventEmitter as EventEmitter31 } from "node:events";
146116
147576
  function toAlias(type, number) {
146117
147577
  return `${type}_${number}`;
146118
147578
  }
@@ -146158,7 +147618,7 @@ var init_github_poll = __esm({
146158
147618
  }
146159
147619
  };
146160
147620
  githubRateLimiter = new GitHubRateLimiter();
146161
- GitHubPollingService = class extends EventEmitter30 {
147621
+ GitHubPollingService = class extends EventEmitter31 {
146162
147622
  watches = /* @__PURE__ */ new Map();
146163
147623
  rateLimiter;
146164
147624
  pollingIntervalMs;
@@ -146403,10 +147863,10 @@ var init_github_poll = __esm({
146403
147863
  });
146404
147864
 
146405
147865
  // ../dashboard/src/pr-metadata-generator.ts
146406
- import { promisify as promisify46 } from "node:util";
147866
+ import { promisify as promisify47 } from "node:util";
146407
147867
  import { exec as execCb } from "node:child_process";
146408
147868
  import { access as access8, readFile as readFile26 } from "node:fs/promises";
146409
- import { join as join54 } from "node:path";
147869
+ import { join as join55 } from "node:path";
146410
147870
  function buildFallback(task) {
146411
147871
  return {
146412
147872
  title: task.title ?? task.id,
@@ -146539,12 +147999,12 @@ async function generatePrMetadata(input) {
146539
147999
  ]);
146540
148000
  let promptContent = "";
146541
148001
  try {
146542
- const promptPath = join54(repoRoot, ".fusion", "tasks", task.id, "PROMPT.md");
148002
+ const promptPath = join55(repoRoot, ".fusion", "tasks", task.id, "PROMPT.md");
146543
148003
  promptContent = (await readFile26(promptPath, "utf8")).trim();
146544
148004
  } catch {
146545
148005
  promptContent = "";
146546
148006
  }
146547
- const templatePath = join54(repoRoot, ".github", "pull_request_template.md");
148007
+ const templatePath = join55(repoRoot, ".github", "pull_request_template.md");
146548
148008
  const templateExists = await access8(templatePath).then(() => true).catch(() => false);
146549
148009
  const template = templateExists ? await readFile26(templatePath, "utf8") : "";
146550
148010
  const model = resolveTaskPlanningModel(task, settings);
@@ -146603,24 +148063,24 @@ var init_pr_metadata_generator = __esm({
146603
148063
  "use strict";
146604
148064
  init_src();
146605
148065
  init_src2();
146606
- execAsync34 = promisify46(execCb);
148066
+ execAsync34 = promisify47(execCb);
146607
148067
  }
146608
148068
  });
146609
148069
 
146610
148070
  // ../dashboard/src/routes/resolve-diff-base.ts
146611
- import { execFile as execFile17 } from "node:child_process";
146612
- import { promisify as promisify47 } from "node:util";
146613
- var execFileAsync12;
148071
+ import { execFile as execFile18 } from "node:child_process";
148072
+ import { promisify as promisify48 } from "node:util";
148073
+ var execFileAsync13;
146614
148074
  var init_resolve_diff_base = __esm({
146615
148075
  "../dashboard/src/routes/resolve-diff-base.ts"() {
146616
148076
  "use strict";
146617
- execFileAsync12 = promisify47(execFile17);
148077
+ execFileAsync13 = promisify48(execFile18);
146618
148078
  }
146619
148079
  });
146620
148080
 
146621
148081
  // ../dashboard/src/routes/register-git-github.ts
146622
148082
  import { exec as execCb2, spawn as spawn12 } from "node:child_process";
146623
- import { promisify as promisify48 } from "node:util";
148083
+ import { promisify as promisify49 } from "node:util";
146624
148084
  var execAsync35, PR_ROUTE_MAX_BUFFER_BYTES;
146625
148085
  var init_register_git_github = __esm({
146626
148086
  "../dashboard/src/routes/register-git-github.ts"() {
@@ -146633,12 +148093,13 @@ var init_register_git_github = __esm({
146633
148093
  init_github_tracking_comments();
146634
148094
  init_github_tracking_state();
146635
148095
  init_github_tracking_reconciler();
148096
+ init_github_source_issue_close();
146636
148097
  init_github_poll();
146637
148098
  init_project_store_resolver();
146638
148099
  init_pr_metadata_generator();
146639
148100
  init_github_webhooks();
146640
148101
  init_resolve_diff_base();
146641
- execAsync35 = promisify48(execCb2);
148102
+ execAsync35 = promisify49(execCb2);
146642
148103
  PR_ROUTE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
146643
148104
  }
146644
148105
  });
@@ -146677,8 +148138,8 @@ var init_register_planning_subtask_routes = __esm({
146677
148138
  });
146678
148139
 
146679
148140
  // ../dashboard/src/chat.ts
146680
- import { EventEmitter as EventEmitter31 } from "node:events";
146681
- import { SessionManager as SessionManager3 } from "@mariozechner/pi-coding-agent";
148141
+ import { EventEmitter as EventEmitter32 } from "node:events";
148142
+ import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
146682
148143
  var defaultDiagnostics, _diagnostics, diagnostics6, RATE_LIMIT_WINDOW_MS5, MAX_REFERENCED_FILE_SIZE, ChatStreamManager, chatStreamManager;
146683
148144
  var init_chat = __esm({
146684
148145
  "../dashboard/src/chat.ts"() {
@@ -146712,7 +148173,7 @@ var init_chat = __esm({
146712
148173
  };
146713
148174
  RATE_LIMIT_WINDOW_MS5 = 60 * 1e3;
146714
148175
  MAX_REFERENCED_FILE_SIZE = 50 * 1024;
146715
- ChatStreamManager = class extends EventEmitter31 {
148176
+ ChatStreamManager = class extends EventEmitter32 {
146716
148177
  constructor(bufferSize = 100) {
146717
148178
  super();
146718
148179
  this.bufferSize = bufferSize;
@@ -151496,7 +152957,7 @@ var init_register_messaging_scripts = __esm({
151496
152957
  // ../dashboard/src/terminal.ts
151497
152958
  import { spawn as spawn13 } from "node:child_process";
151498
152959
  import { randomUUID as randomUUID25 } from "node:crypto";
151499
- import { EventEmitter as EventEmitter32 } from "node:events";
152960
+ import { EventEmitter as EventEmitter33 } from "node:events";
151500
152961
  function extractBaseCommand(command) {
151501
152962
  let trimmed = command.trim();
151502
152963
  while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
@@ -151642,7 +153103,7 @@ var init_terminal = __esm({
151642
153103
  // >(...) — process substitution (bash)
151643
153104
  ];
151644
153105
  CHAIN_OPERATORS = /&&|\|\||;|(?<!\|)\|(?!\|)/;
151645
- TerminalSessionManager = class extends EventEmitter32 {
153106
+ TerminalSessionManager = class extends EventEmitter33 {
151646
153107
  sessions = /* @__PURE__ */ new Map();
151647
153108
  defaultTimeout = 3e4;
151648
153109
  // 30 seconds
@@ -151850,19 +153311,19 @@ var init_register_agents_projects_nodes = __esm({
151850
153311
  });
151851
153312
 
151852
153313
  // ../dashboard/src/exec-file.ts
151853
- import { execFile as execFile18 } from "node:child_process";
151854
- import { promisify as promisify49 } from "node:util";
151855
- var execFileAsync13;
153314
+ import { execFile as execFile19 } from "node:child_process";
153315
+ import { promisify as promisify50 } from "node:util";
153316
+ var execFileAsync14;
151856
153317
  var init_exec_file = __esm({
151857
153318
  "../dashboard/src/exec-file.ts"() {
151858
153319
  "use strict";
151859
- execFileAsync13 = promisify49(execFile18);
153320
+ execFileAsync14 = promisify50(execFile19);
151860
153321
  }
151861
153322
  });
151862
153323
 
151863
153324
  // ../dashboard/src/routes/register-project-routes.ts
151864
153325
  import * as fsPromises from "node:fs/promises";
151865
- var access9, stat9, mkdir16, readdir9, rm7;
153326
+ var access9, stat9, mkdir16, readdir9, rm8;
151866
153327
  var init_register_project_routes = __esm({
151867
153328
  "../dashboard/src/routes/register-project-routes.ts"() {
151868
153329
  "use strict";
@@ -151875,7 +153336,7 @@ var init_register_project_routes = __esm({
151875
153336
  stat: stat9,
151876
153337
  mkdir: mkdir16,
151877
153338
  readdir: readdir9,
151878
- rm: rm7
153339
+ rm: rm8
151879
153340
  } = fsPromises);
151880
153341
  }
151881
153342
  });
@@ -152071,7 +153532,7 @@ var init_agent_generation = __esm({
152071
153532
 
152072
153533
  // ../dashboard/src/routes/register-agent-import-export-generation-routes.ts
152073
153534
  import * as fsPromises2 from "node:fs/promises";
152074
- var mkdtemp3, access10, stat10, mkdir17, rm8, fsWriteFile;
153535
+ var mkdtemp4, access10, stat10, mkdir17, rm9, fsWriteFile;
152075
153536
  var init_register_agent_import_export_generation_routes = __esm({
152076
153537
  "../dashboard/src/routes/register-agent-import-export-generation-routes.ts"() {
152077
153538
  "use strict";
@@ -152079,7 +153540,7 @@ var init_register_agent_import_export_generation_routes = __esm({
152079
153540
  init_ai_session_diagnostics();
152080
153541
  init_sse_buffer();
152081
153542
  init_agent_generation();
152082
- ({ mkdtemp: mkdtemp3, access: access10, stat: stat10, mkdir: mkdir17, rm: rm8, writeFile: fsWriteFile } = fsPromises2);
153543
+ ({ mkdtemp: mkdtemp4, access: access10, stat: stat10, mkdir: mkdir17, rm: rm9, writeFile: fsWriteFile } = fsPromises2);
152083
153544
  }
152084
153545
  });
152085
153546
 
@@ -153349,6 +154810,16 @@ var init_todo_routes = __esm({
153349
154810
  }
153350
154811
  });
153351
154812
 
154813
+ // ../dashboard/src/goals-routes.ts
154814
+ import { Router as Router9 } from "express";
154815
+ var init_goals_routes = __esm({
154816
+ "../dashboard/src/goals-routes.ts"() {
154817
+ "use strict";
154818
+ init_api_error();
154819
+ init_project_store_resolver();
154820
+ }
154821
+ });
154822
+
153352
154823
  // ../../plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js
153353
154824
  function compareOrderedEntities(a, b2) {
153354
154825
  if (a.orderIndex !== b2.orderIndex) {
@@ -153514,13 +154985,13 @@ var init_roadmap_ordering = __esm({
153514
154985
  });
153515
154986
 
153516
154987
  // ../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js
153517
- import { EventEmitter as EventEmitter33 } from "node:events";
154988
+ import { EventEmitter as EventEmitter34 } from "node:events";
153518
154989
  var RoadmapStore;
153519
154990
  var init_roadmap_store = __esm({
153520
154991
  "../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js"() {
153521
154992
  "use strict";
153522
154993
  init_roadmap_ordering();
153523
- RoadmapStore = class extends EventEmitter33 {
154994
+ RoadmapStore = class extends EventEmitter34 {
153524
154995
  db;
153525
154996
  /**
153526
154997
  * Creates a new RoadmapStore instance.
@@ -155024,7 +156495,7 @@ var init_src5 = __esm({
155024
156495
  });
155025
156496
 
155026
156497
  // ../dashboard/src/roadmap-routes.ts
155027
- import { Router as Router9 } from "express";
156498
+ import { Router as Router10 } from "express";
155028
156499
  var init_roadmap_routes3 = __esm({
155029
156500
  "../dashboard/src/roadmap-routes.ts"() {
155030
156501
  "use strict";
@@ -155068,7 +156539,7 @@ var init_dev_server_process = __esm({
155068
156539
  });
155069
156540
 
155070
156541
  // ../dashboard/src/dev-server-routes.ts
155071
- import { Router as Router10 } from "express";
156542
+ import { Router as Router11 } from "express";
155072
156543
  var init_dev_server_routes = __esm({
155073
156544
  "../dashboard/src/dev-server-routes.ts"() {
155074
156545
  "use strict";
@@ -155080,7 +156551,7 @@ var init_dev_server_routes = __esm({
155080
156551
  });
155081
156552
 
155082
156553
  // ../dashboard/src/routes/register-stash-recovery-routes.ts
155083
- import { Router as Router11 } from "express";
156554
+ import { Router as Router12 } from "express";
155084
156555
  var init_register_stash_recovery_routes = __esm({
155085
156556
  "../dashboard/src/routes/register-stash-recovery-routes.ts"() {
155086
156557
  "use strict";
@@ -155099,6 +156570,7 @@ var init_register_integrated_routers = __esm({
155099
156570
  init_research_routes();
155100
156571
  init_experiment_routes();
155101
156572
  init_todo_routes();
156573
+ init_goals_routes();
155102
156574
  init_roadmap_routes3();
155103
156575
  init_dev_server_routes();
155104
156576
  init_register_stash_recovery_routes();
@@ -157463,7 +158935,7 @@ var require_extension = __commonJS({
157463
158935
  var require_websocket = __commonJS({
157464
158936
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports, module) {
157465
158937
  "use strict";
157466
- var EventEmitter34 = __require("events");
158938
+ var EventEmitter35 = __require("events");
157467
158939
  var https = __require("https");
157468
158940
  var http = __require("http");
157469
158941
  var net2 = __require("net");
@@ -157495,7 +158967,7 @@ var require_websocket = __commonJS({
157495
158967
  var protocolVersions = [8, 13];
157496
158968
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
157497
158969
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
157498
- var WebSocket2 = class _WebSocket extends EventEmitter34 {
158970
+ var WebSocket2 = class _WebSocket extends EventEmitter35 {
157499
158971
  /**
157500
158972
  * Create a new `WebSocket`.
157501
158973
  *
@@ -158492,7 +159964,7 @@ var require_subprotocol = __commonJS({
158492
159964
  var require_websocket_server = __commonJS({
158493
159965
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports, module) {
158494
159966
  "use strict";
158495
- var EventEmitter34 = __require("events");
159967
+ var EventEmitter35 = __require("events");
158496
159968
  var http = __require("http");
158497
159969
  var { Duplex } = __require("stream");
158498
159970
  var { createHash: createHash15 } = __require("crypto");
@@ -158505,7 +159977,7 @@ var require_websocket_server = __commonJS({
158505
159977
  var RUNNING = 0;
158506
159978
  var CLOSING = 1;
158507
159979
  var CLOSED = 2;
158508
- var WebSocketServer2 = class extends EventEmitter34 {
159980
+ var WebSocketServer2 = class extends EventEmitter35 {
158509
159981
  /**
158510
159982
  * Create a `WebSocketServer` instance.
158511
159983
  *
@@ -158943,7 +160415,7 @@ var init_view_chunk_manifest = __esm({
158943
160415
 
158944
160416
  // ../dashboard/src/server.ts
158945
160417
  import express from "express";
158946
- import { join as join55, dirname as dirname21 } from "node:path";
160418
+ import { join as join56, dirname as dirname21 } from "node:path";
158947
160419
  import { fileURLToPath as fileURLToPath6 } from "node:url";
158948
160420
  function clearAiSessionCleanupInterval() {
158949
160421
  if (!aiSessionCleanupIntervalHandle) {
@@ -159052,6 +160524,7 @@ var init_src6 = __esm({
159052
160524
  init_rate_limit();
159053
160525
  init_github_poll();
159054
160526
  init_github_issue_comment();
160527
+ init_github_source_issue_close();
159055
160528
  init_github_tracking_comments();
159056
160529
  init_github_tracking_state();
159057
160530
  init_github_tracking_reconciler();
@@ -159248,7 +160721,7 @@ __export(task_exports, {
159248
160721
  });
159249
160722
  import { createInterface as createInterface2 } from "node:readline/promises";
159250
160723
  import { watchFile, unwatchFile, statSync as statSync7, existsSync as existsSync44, readFileSync as readFileSync15 } from "node:fs";
159251
- import { basename as basename16, join as join56 } from "node:path";
160724
+ import { basename as basename16, join as join57 } from "node:path";
159252
160725
  function formatGhErrorForCli(err) {
159253
160726
  const structured = classifyGhError(err);
159254
160727
  const lines = [`GitHub error: ${structured.message}`];
@@ -159709,7 +161182,7 @@ async function runTaskLogs(id, options = {}, projectName) {
159709
161182
  printEntries(filteredEntries);
159710
161183
  if (options.follow) {
159711
161184
  const projectPath = projectContext?.projectPath ?? process.cwd();
159712
- const logPath = join56(projectPath, ".fusion", "tasks", id, "agent.log");
161185
+ const logPath = join57(projectPath, ".fusion", "tasks", id, "agent.log");
159713
161186
  if (!existsSync44(logPath)) {
159714
161187
  console.log(`
159715
161188
  Waiting for log file to be created...`);
@@ -160887,8 +162360,8 @@ init_gh_cli();
160887
162360
  init_src2();
160888
162361
  init_src6();
160889
162362
  import { Type as Type8 } from "typebox";
160890
- import { StringEnum } from "@mariozechner/pi-ai";
160891
- import { resolve as resolve31, basename as basename17, extname as extname3, join as join57 } from "node:path";
162363
+ import { StringEnum } from "@earendil-works/pi-ai";
162364
+ import { resolve as resolve31, basename as basename17, extname as extname3, join as join58 } from "node:path";
160892
162365
  import { readFile as readFile28 } from "node:fs/promises";
160893
162366
  import { existsSync as existsSync45 } from "node:fs";
160894
162367
  import { spawn as spawn16 } from "node:child_process";
@@ -160914,12 +162387,12 @@ function resolveProjectRoot2(cwd) {
160914
162387
  warnedMissingProjectRootResolver = true;
160915
162388
  console.warn("[fusion-extension] @fusion/core.getProjectRootFromWorktree is unavailable; using filesystem fallback resolution.");
160916
162389
  }
160917
- if (worktreeProjectRoot && existsSync45(join57(worktreeProjectRoot, ".fusion"))) {
162390
+ if (worktreeProjectRoot && existsSync45(join58(worktreeProjectRoot, ".fusion"))) {
160918
162391
  return worktreeProjectRoot;
160919
162392
  }
160920
162393
  let current = resolve31(cwd);
160921
162394
  while (true) {
160922
- if (existsSync45(join57(current, ".fusion"))) {
162395
+ if (existsSync45(join58(current, ".fusion"))) {
160923
162396
  return current;
160924
162397
  }
160925
162398
  const parent = resolve31(current, "..");
@@ -160940,7 +162413,7 @@ async function getStore2(cwd) {
160940
162413
  return store;
160941
162414
  }
160942
162415
  function getFusionDir(cwd) {
160943
- return join57(resolveProjectRoot2(cwd), ".fusion");
162416
+ return join58(resolveProjectRoot2(cwd), ".fusion");
160944
162417
  }
160945
162418
  function emitSecretAudit(store, ctx, mutationType, target, metadata) {
160946
162419
  if (!ctx.runId || !ctx.agentId) return;
@@ -162804,6 +164277,174 @@ Status: ${createdMission.status}${createdMission.autoAdvance ? "\nAuto-advance:
162804
164277
  };
162805
164278
  }
162806
164279
  });
164280
+ pi2.registerTool({
164281
+ name: "fn_goal_list",
164282
+ label: "fn: List Goals",
164283
+ description: "List goals by status with active-goal warning details.",
164284
+ promptSnippet: "List project goals",
164285
+ promptGuidelines: [
164286
+ "Use to inspect current goals before creating or archiving",
164287
+ "Default status is active; pass archived/all when needed",
164288
+ "Soft warning begins at 3 active goals and hard cap is 5"
164289
+ ],
164290
+ parameters: Type8.Object({
164291
+ status: Type8.Optional(
164292
+ Type8.Union([
164293
+ Type8.Literal("active"),
164294
+ Type8.Literal("archived"),
164295
+ Type8.Literal("all")
164296
+ ], { description: "Filter by goal status (default: active)" })
164297
+ )
164298
+ }),
164299
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164300
+ const store = await getStore2(ctx.cwd);
164301
+ const goalStore = store.getGoalStore();
164302
+ const status = params.status ?? "active";
164303
+ const goals = status === "all" ? goalStore.listGoals() : goalStore.listGoals({ status });
164304
+ const activeCount = goalStore.listGoals({ status: "active" }).length;
164305
+ const softWarning = activeCount >= 3;
164306
+ const lines = [];
164307
+ lines.push(`Goals (${goals.length}) [filter: ${status}]`);
164308
+ lines.push(`Active: ${activeCount}/5`);
164309
+ if (softWarning) {
164310
+ lines.push("\u26A0 3/5 active goals \u2014 soft warning at 3, hard cap at 5");
164311
+ }
164312
+ lines.push("");
164313
+ if (goals.length === 0) {
164314
+ lines.push("No goals found.");
164315
+ } else {
164316
+ for (const goal of goals) {
164317
+ const description = goal.description ? ` \u2014 ${goal.description}` : "";
164318
+ lines.push(`- ${goal.id} [${goal.status}] ${goal.title}${description}`);
164319
+ }
164320
+ }
164321
+ return {
164322
+ content: [{ type: "text", text: lines.join("\n") }],
164323
+ details: { goals, activeCount, softWarning, hardLimit: 5 }
164324
+ };
164325
+ }
164326
+ });
164327
+ pi2.registerTool({
164328
+ name: "fn_goal_create",
164329
+ label: "fn: Create Goal",
164330
+ description: "Create a new project goal.",
164331
+ promptSnippet: "Create a new goal",
164332
+ promptGuidelines: [
164333
+ "Use clear titles and optional context-rich descriptions",
164334
+ "Goal creation counts toward the 5 active-goal hard cap",
164335
+ "Archive older goals when the active cap is reached"
164336
+ ],
164337
+ parameters: Type8.Object({
164338
+ title: Type8.String({ description: "Goal title \u2014 brief but descriptive" }),
164339
+ description: Type8.Optional(
164340
+ Type8.String({ description: "Long-form goal description (free-text markdown)" })
164341
+ )
164342
+ }),
164343
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164344
+ const store = await getStore2(ctx.cwd);
164345
+ const goalStore = store.getGoalStore();
164346
+ try {
164347
+ const goal = goalStore.createGoal({
164348
+ title: params.title.trim(),
164349
+ description: params.description?.trim() || void 0
164350
+ });
164351
+ const activeCount = goalStore.listGoals({ status: "active" }).length;
164352
+ const softWarning = activeCount >= 3;
164353
+ return {
164354
+ content: [{ type: "text", text: `Created ${goal.id}: ${goal.title}
164355
+ Status: ${goal.status}${softWarning ? `
164356
+ \u26A0 ${activeCount}/5 active goals \u2014 approaching hard cap` : ""}` }],
164357
+ details: { goalId: goal.id, title: goal.title, status: goal.status, softWarning }
164358
+ };
164359
+ } catch (err) {
164360
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ACTIVE_GOAL_LIMIT_EXCEEDED") {
164361
+ const limit = err.limit ?? 5;
164362
+ const currentActive = err.currentActive ?? 5;
164363
+ return {
164364
+ isError: true,
164365
+ content: [{ type: "text", text: `Cannot create goal \u2014 already at the hard cap of 5 active goals (currently ${currentActive}). Archive one first.` }],
164366
+ details: { code: "ACTIVE_GOAL_LIMIT_EXCEEDED", limit, currentActive }
164367
+ };
164368
+ }
164369
+ throw err;
164370
+ }
164371
+ }
164372
+ });
164373
+ pi2.registerTool({
164374
+ name: "fn_goal_archive",
164375
+ label: "fn: Archive Goal",
164376
+ description: "Archive a goal by ID.",
164377
+ promptSnippet: "Archive a goal by ID",
164378
+ promptGuidelines: [
164379
+ "Use when a goal is complete or no longer active",
164380
+ "Archiving frees active-goal capacity",
164381
+ "Returns success for already archived goals"
164382
+ ],
164383
+ parameters: Type8.Object({
164384
+ id: Type8.String({ description: "Goal ID (G-\u2026) to archive" })
164385
+ }),
164386
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164387
+ const store = await getStore2(ctx.cwd);
164388
+ const goalStore = store.getGoalStore();
164389
+ const goal = goalStore.getGoal(params.id);
164390
+ if (!goal) {
164391
+ return {
164392
+ isError: true,
164393
+ content: [{ type: "text", text: `Goal ${params.id} not found` }],
164394
+ details: { code: "GOAL_NOT_FOUND", goalId: params.id }
164395
+ };
164396
+ }
164397
+ if (goal.status === "archived") {
164398
+ return {
164399
+ content: [{ type: "text", text: `Goal ${params.id} is already archived` }],
164400
+ details: { goalId: params.id, status: "archived" }
164401
+ };
164402
+ }
164403
+ const archived = goalStore.archiveGoal(params.id);
164404
+ return {
164405
+ content: [{ type: "text", text: `Archived ${archived.id}: ${archived.title}` }],
164406
+ details: { goalId: archived.id, status: "archived" }
164407
+ };
164408
+ }
164409
+ });
164410
+ pi2.registerTool({
164411
+ name: "fn_goal_show",
164412
+ label: "fn: Show Goal",
164413
+ description: "Show full details for a single goal by ID.",
164414
+ promptSnippet: "Show goal details by ID",
164415
+ promptGuidelines: [
164416
+ "Use to inspect a specific goal after listing or referencing its ID",
164417
+ "Cite the goal ID and status when summarizing or planning from this output",
164418
+ "Read-only retrieval; does not mutate goals"
164419
+ ],
164420
+ parameters: Type8.Object({
164421
+ id: Type8.String({ description: "Goal ID (G-\u2026)" })
164422
+ }),
164423
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164424
+ const store = await getStore2(ctx.cwd);
164425
+ const goalStore = store.getGoalStore();
164426
+ const goal = goalStore.getGoal(params.id);
164427
+ if (!goal) {
164428
+ return {
164429
+ isError: true,
164430
+ content: [{ type: "text", text: `Goal ${params.id} not found` }],
164431
+ details: { code: "GOAL_NOT_FOUND", goalId: params.id }
164432
+ };
164433
+ }
164434
+ const lines = [];
164435
+ lines.push(`${goal.id}: ${goal.title}`);
164436
+ lines.push(`Status: ${goal.status}`);
164437
+ lines.push(`Created: ${goal.createdAt}`);
164438
+ lines.push(`Updated: ${goal.updatedAt}`);
164439
+ if (goal.description) {
164440
+ lines.push(`Description: ${goal.description}`);
164441
+ }
164442
+ return {
164443
+ content: [{ type: "text", text: lines.join("\n") }],
164444
+ details: { goal }
164445
+ };
164446
+ }
164447
+ });
162807
164448
  pi2.registerTool({
162808
164449
  name: "fn_mission_show",
162809
164450
  label: "fn: Show Mission",