@runfusion/fusion 0.34.0 → 0.36.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 (65) hide show
  1. package/dist/bin.js +2790 -633
  2. package/dist/client/.vite/manifest.json +56 -56
  3. package/dist/client/assets/{AgentDetailView-CzqwIIpv.js → AgentDetailView-CGycokdR.js} +1 -1
  4. package/dist/client/assets/{AgentPermissionPolicyEditor-2eoM2XQH.js → AgentPermissionPolicyEditor-BoD6come.js} +1 -1
  5. package/dist/client/assets/{AgentsView-BrtAvhJl.js → AgentsView-B_afRRfc.js} +3 -3
  6. package/dist/client/assets/ChatView-IjNrSZSf.js +6 -0
  7. package/dist/client/assets/{DevServerView-CF6FPLRb.js → DevServerView-Dd613aZ5.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-BpvHQxhu.js → DirectoryPicker-H-fUl2dF.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-RMWZBJ-y.js → DocumentsView-Dshc9e5K.js} +1 -1
  10. package/dist/client/assets/{EvalsView-DtVxpc4T.js → EvalsView-50W0K5h7.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-fzJmb-yG.js → ExperimentalAgentOnboardingModal-BXBljPFj.js} +1 -1
  12. package/dist/client/assets/{GoalsView-BAchggxo.js → GoalsView-Dh508Z2s.js} +1 -1
  13. package/dist/client/assets/{InsightsView-B8892IbG.js → InsightsView-sVbGRKTl.js} +1 -1
  14. package/dist/client/assets/{MemoryView-Cfp1WmAm.js → MemoryView-006a_sB0.js} +1 -1
  15. package/dist/client/assets/{NodesView-Dt26DSEQ.js → NodesView-CiRPqgVx.js} +1 -1
  16. package/dist/client/assets/{PiExtensionsManager-xHU56SMh.js → PiExtensionsManager-Y1ETuTxf.js} +1 -1
  17. package/dist/client/assets/{PluginManager-37WUEOmp.js → PluginManager-Df_KHiXn.js} +1 -1
  18. package/dist/client/assets/{ResearchView-pu88M5xL.js → ResearchView-DU_QuS_-.js} +1 -1
  19. package/dist/client/assets/{SecretsView-BT7ofJLN.js → SecretsView-D61Jvysn.js} +1 -1
  20. package/dist/client/assets/{SettingsModal-DxYXsn08.js → SettingsModal-BarzmqNb.js} +1 -1
  21. package/dist/client/assets/SettingsModal-Cpur3WpT.js +31 -0
  22. package/dist/client/assets/{SetupWizardModal-yWTk3Cac.js → SetupWizardModal-BF3iRNq9.js} +1 -1
  23. package/dist/client/assets/{SkillsView-CWNfKNTs.js → SkillsView-tCKOFw3j.js} +1 -1
  24. package/dist/client/assets/{StashRecoveryView-D0t4IIX-.js → StashRecoveryView-nGtypd10.js} +1 -1
  25. package/dist/client/assets/{TodoView-B4W1Lhy6.js → TodoView-IyGPV6AO.js} +1 -1
  26. package/dist/client/assets/{dashboard-view-4OCJIthP.js → dashboard-view-DiA5-hnb.js} +1 -1
  27. package/dist/client/assets/{folder-open-BTdhwFQ5.js → folder-open-Byqfi_N-.js} +1 -1
  28. package/dist/client/assets/index-B9Ro_NwC.js +742 -0
  29. package/dist/client/assets/index-DVFBXjGT.css +1 -0
  30. package/dist/client/assets/{upload-kESyjow_.js → upload-D6_BjqVi.js} +1 -1
  31. package/dist/client/assets/{users-DNXMrdcp.js → users-CERYgInO.js} +1 -1
  32. package/dist/client/index.html +2 -2
  33. package/dist/client/version.json +1 -1
  34. package/dist/droid-cli/index.ts +1 -1
  35. package/dist/droid-cli/package.json +3 -3
  36. package/dist/droid-cli/src/__tests__/thinking-config.test.ts +1 -1
  37. package/dist/extension.js +1959 -310
  38. package/dist/pi-claude-cli/index.ts +3 -3
  39. package/dist/pi-claude-cli/package.json +3 -3
  40. package/dist/pi-claude-cli/src/__tests__/event-bridge.test.ts +3 -3
  41. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +3 -3
  42. package/dist/pi-claude-cli/src/__tests__/thinking-config.test.ts +1 -1
  43. package/dist/pi-claude-cli/src/event-bridge.ts +2 -2
  44. package/dist/pi-claude-cli/src/provider.ts +1 -1
  45. package/dist/pi-claude-cli/src/thinking-config.ts +1 -1
  46. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  47. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  48. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +34607 -33421
  50. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  51. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  52. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  53. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  54. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  55. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  56. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  57. package/package.json +7 -7
  58. package/skill/fusion/SKILL.md +1 -0
  59. package/skill/fusion/references/cli-commands.md +3 -0
  60. package/skill/fusion/references/extension-tools.md +27 -0
  61. package/skill/fusion/references/fusion-capabilities.md +3 -0
  62. package/dist/client/assets/ChatView-BuIe4MhL.js +0 -6
  63. package/dist/client/assets/SettingsModal-Cz9MHNGg.js +0 -31
  64. package/dist/client/assets/index-D4nQgFjo.js +0 -742
  65. package/dist/client/assets/index-k4s0JWir.css +0 -1
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",
@@ -2070,7 +2076,31 @@ Do NOT spend time on nits when no real issues exist.`
2070
2076
  ];
2071
2077
  DEFAULT_HEARTBEAT_PROCEDURE_PATH = ".fusion/HEARTBEAT.md";
2072
2078
  DASHBOARD_USER_ID = "dashboard";
2073
- DASHBOARD_USER_ALIASES = /* @__PURE__ */ new Set([DASHBOARD_USER_ID, "user:dashboard", "User: user:dashboard"]);
2079
+ DASHBOARD_USER_ALIASES = /* @__PURE__ */ new Set([DASHBOARD_USER_ID, "user", "user:dashboard", "User: user:dashboard"]);
2080
+ }
2081
+ });
2082
+
2083
+ // ../core/src/custom-provider-key.ts
2084
+ function slugifyProviderName(name) {
2085
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
2086
+ }
2087
+ function customProviderRegistryKey(provider, allProviders) {
2088
+ const base = slugifyProviderName(provider.name) || provider.id;
2089
+ let count = 0;
2090
+ for (const current of allProviders) {
2091
+ const currentBase = slugifyProviderName(current.name) || current.id;
2092
+ if (currentBase === base) {
2093
+ count += 1;
2094
+ }
2095
+ if (current.id === provider.id) {
2096
+ break;
2097
+ }
2098
+ }
2099
+ return count <= 1 ? base : `${base}-${count}`;
2100
+ }
2101
+ var init_custom_provider_key = __esm({
2102
+ "../core/src/custom-provider-key.ts"() {
2103
+ "use strict";
2074
2104
  }
2075
2105
  });
2076
2106
 
@@ -4642,7 +4672,7 @@ var init_db = __esm({
4642
4672
  DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
4643
4673
  DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
4644
4674
  DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
4645
- SCHEMA_VERSION = 91;
4675
+ SCHEMA_VERSION = 92;
4646
4676
  SCHEMA_SQL = `
4647
4677
  -- Tasks table with JSON columns for nested data
4648
4678
  CREATE TABLE IF NOT EXISTS tasks (
@@ -5195,6 +5225,17 @@ CREATE TABLE IF NOT EXISTS missions (
5195
5225
  updatedAt TEXT NOT NULL
5196
5226
  );
5197
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
+
5198
5239
  -- Milestones table (phases within a mission)
5199
5240
  CREATE TABLE IF NOT EXISTS milestones (
5200
5241
  id TEXT PRIMARY KEY,
@@ -7507,6 +7548,24 @@ This means a caller passed a .fusion directory where a project root was expected
7507
7548
  this.addColumnIfMissing("missions", "baseBranch", "TEXT");
7508
7549
  });
7509
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
+ }
7510
7569
  }
7511
7570
  /**
7512
7571
  * Run a single migration step inside a transaction and bump the version.
@@ -10497,7 +10556,7 @@ var init_message_store = __esm({
10497
10556
  }
10498
10557
  getParticipantIdsForLookup(ownerId, ownerType) {
10499
10558
  if (ownerType === "user" && ownerId === DASHBOARD_USER_ID) {
10500
- return [DASHBOARD_USER_ID, "user:dashboard", "User: user:dashboard"];
10559
+ return [DASHBOARD_USER_ID, "user", "user:dashboard", "User: user:dashboard"];
10501
10560
  }
10502
10561
  return [ownerId];
10503
10562
  }
@@ -18630,6 +18689,158 @@ var init_todo_store = __esm({
18630
18689
  }
18631
18690
  });
18632
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
+
18633
18844
  // ../core/src/eval-types.ts
18634
18845
  function normalizeEvalFollowUpText(value) {
18635
18846
  return value.toLowerCase().replace(/\s+/g, " ").replace(/[^a-z0-9 ]/g, "").trim();
@@ -18708,7 +18919,7 @@ var init_eval_types = __esm({
18708
18919
  });
18709
18920
 
18710
18921
  // ../core/src/eval-store.ts
18711
- import { EventEmitter as EventEmitter10 } from "node:events";
18922
+ import { EventEmitter as EventEmitter11 } from "node:events";
18712
18923
  import { randomUUID as randomUUID9 } from "node:crypto";
18713
18924
  function generateRunId3() {
18714
18925
  return `ER-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`;
@@ -18804,7 +19015,7 @@ var init_eval_store = __esm({
18804
19015
  this.name = "EvalLifecycleError";
18805
19016
  }
18806
19017
  };
18807
- EvalStore = class extends EventEmitter10 {
19018
+ EvalStore = class extends EventEmitter11 {
18808
19019
  constructor(db) {
18809
19020
  super();
18810
19021
  this.db = db;
@@ -19764,7 +19975,7 @@ var require_registry = __commonJS({
19764
19975
  setTimeout(send, Math.random() * 250);
19765
19976
  }
19766
19977
  announce(server, service) {
19767
- var delay4 = 1e3;
19978
+ var delay5 = 1e3;
19768
19979
  var packet = service.records();
19769
19980
  server.register(packet);
19770
19981
  const broadcast = () => {
@@ -19776,9 +19987,9 @@ var require_registry = __commonJS({
19776
19987
  service.published = true;
19777
19988
  service.emit("up");
19778
19989
  }
19779
- delay4 = delay4 * REANNOUNCE_FACTOR;
19780
- if (delay4 < REANNOUNCE_MAX_MS && !service.destroyed) {
19781
- setTimeout(broadcast, delay4).unref();
19990
+ delay5 = delay5 * REANNOUNCE_FACTOR;
19991
+ if (delay5 < REANNOUNCE_MAX_MS && !service.destroyed) {
19992
+ setTimeout(broadcast, delay5).unref();
19782
19993
  }
19783
19994
  });
19784
19995
  };
@@ -22604,7 +22815,7 @@ var require_dist = __commonJS({
22604
22815
  });
22605
22816
 
22606
22817
  // ../core/src/node-discovery.ts
22607
- import { EventEmitter as EventEmitter11 } from "node:events";
22818
+ import { EventEmitter as EventEmitter12 } from "node:events";
22608
22819
  import os from "node:os";
22609
22820
  var import_bonjour_service, DEFAULT_DISCOVERY_CONFIG, STALE_CLEANUP_INTERVAL_MS, FUSION_VERSION, NodeDiscovery;
22610
22821
  var init_node_discovery = __esm({
@@ -22620,7 +22831,7 @@ var init_node_discovery = __esm({
22620
22831
  };
22621
22832
  STALE_CLEANUP_INTERVAL_MS = 6e4;
22622
22833
  FUSION_VERSION = "0.1.0";
22623
- NodeDiscovery = class extends EventEmitter11 {
22834
+ NodeDiscovery = class extends EventEmitter12 {
22624
22835
  config;
22625
22836
  bonjour = null;
22626
22837
  broadcastService = null;
@@ -23156,7 +23367,7 @@ var init_project_identity = __esm({
23156
23367
  });
23157
23368
 
23158
23369
  // ../core/src/central-core.ts
23159
- import { EventEmitter as EventEmitter12 } from "node:events";
23370
+ import { EventEmitter as EventEmitter13 } from "node:events";
23160
23371
  import { createHash as createHash5, randomUUID as randomUUID10 } from "node:crypto";
23161
23372
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
23162
23373
  import { mkdir as mkdir4 } from "node:fs/promises";
@@ -23174,7 +23385,7 @@ var init_central_core = __esm({
23174
23385
  init_system_metrics();
23175
23386
  init_shared_mesh_state();
23176
23387
  init_project_identity();
23177
- CentralCore = class extends EventEmitter12 {
23388
+ CentralCore = class extends EventEmitter13 {
23178
23389
  db = null;
23179
23390
  globalDir;
23180
23391
  initialized = false;
@@ -25345,10 +25556,10 @@ var init_central_core = __esm({
25345
25556
  */
25346
25557
  async generateProjectName(projectPath) {
25347
25558
  try {
25348
- const { execFile: execFile18 } = await import("node:child_process");
25349
- const { promisify: promisify50 } = await import("node:util");
25350
- const execFileAsync14 = promisify50(execFile18);
25351
- 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(
25352
25563
  "git",
25353
25564
  ["remote", "get-url", "origin"],
25354
25565
  { cwd: projectPath, timeout: 5e3 }
@@ -26052,10 +26263,10 @@ var init_migration = __esm({
26052
26263
  return basename5(projectPath);
26053
26264
  }
26054
26265
  try {
26055
- const { execFile: execFile18 } = await import("node:child_process");
26056
- const { promisify: promisify50 } = await import("node:util");
26057
- const execFileAsync14 = promisify50(execFile18);
26058
- 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(
26059
26270
  "git",
26060
26271
  ["remote", "get-url", "origin"],
26061
26272
  { cwd: projectPath, timeout: 1e3 }
@@ -36699,7 +36910,7 @@ var automation_store_exports = {};
36699
36910
  __export(automation_store_exports, {
36700
36911
  AutomationStore: () => AutomationStore
36701
36912
  });
36702
- import { EventEmitter as EventEmitter13 } from "node:events";
36913
+ import { EventEmitter as EventEmitter14 } from "node:events";
36703
36914
  import { join as join16 } from "node:path";
36704
36915
  import { randomUUID as randomUUID12 } from "node:crypto";
36705
36916
  var import_cron_parser, CRON_TIMEZONE, AutomationStore;
@@ -36711,7 +36922,7 @@ var init_automation_store = __esm({
36711
36922
  init_db();
36712
36923
  init_project_root_guard();
36713
36924
  CRON_TIMEZONE = "UTC";
36714
- AutomationStore = class _AutomationStore extends EventEmitter13 {
36925
+ AutomationStore = class _AutomationStore extends EventEmitter14 {
36715
36926
  constructor(rootDir, options) {
36716
36927
  super();
36717
36928
  this.rootDir = rootDir;
@@ -37803,13 +38014,13 @@ async function searchWithQmd(rootDir, options) {
37803
38014
  const command = "qmd";
37804
38015
  const limit = Math.max(1, Math.min(options.limit ?? 5, 20));
37805
38016
  try {
37806
- const { execFile: execFile18 } = await import("node:child_process");
37807
- const { promisify: promisify50 } = await import("node:util");
37808
- const execFileAsync14 = promisify50(execFile18);
37809
- 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);
37810
38021
  scheduleQmdProjectMemoryRefresh(rootDir);
37811
38022
  const args = buildQmdSearchArgs(rootDir, options);
37812
- const { stdout } = await execFileAsync14(command, args, {
38023
+ const { stdout } = await execFileAsync15(command, args, {
37813
38024
  cwd: rootDir,
37814
38025
  timeout: 4e3,
37815
38026
  maxBuffer: 1024 * 1024
@@ -37834,12 +38045,12 @@ async function searchWithQmd(rootDir, options) {
37834
38045
  return [];
37835
38046
  }
37836
38047
  }
37837
- async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync14) {
38048
+ async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync15) {
37838
38049
  const collectionName = qmdMemoryCollectionName(rootDir);
37839
38050
  const memoryDir = memoryWorkspacePath(rootDir);
37840
38051
  await mkdir6(memoryDir, { recursive: true });
37841
38052
  try {
37842
- await execFileAsync14("qmd", buildQmdCollectionAddArgs(rootDir), {
38053
+ await execFileAsync15("qmd", buildQmdCollectionAddArgs(rootDir), {
37843
38054
  cwd: rootDir,
37844
38055
  timeout: 4e3,
37845
38056
  maxBuffer: 512 * 1024
@@ -37855,9 +38066,9 @@ ${stderr}`)) {
37855
38066
  return collectionName;
37856
38067
  }
37857
38068
  async function getDefaultExecFileAsync() {
37858
- const { execFile: execFile18 } = await import("node:child_process");
37859
- const { promisify: promisify50 } = await import("node:util");
37860
- return promisify50(execFile18);
38069
+ const { execFile: execFile20 } = await import("node:child_process");
38070
+ const { promisify: promisify51 } = await import("node:util");
38071
+ return promisify51(execFile20);
37861
38072
  }
37862
38073
  async function refreshQmdProjectMemoryIndex(rootDir, options) {
37863
38074
  const key = resolve8(rootDir);
@@ -37872,14 +38083,14 @@ async function refreshQmdProjectMemoryIndex(rootDir, options) {
37872
38083
  }
37873
38084
  }
37874
38085
  const promise = (async () => {
37875
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37876
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync14);
37877
- await execFileAsync14("qmd", ["update"], {
38086
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38087
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync15);
38088
+ await execFileAsync15("qmd", ["update"], {
37878
38089
  cwd: rootDir,
37879
38090
  timeout: 3e4,
37880
38091
  maxBuffer: 1024 * 1024
37881
38092
  });
37882
- await execFileAsync14("qmd", ["embed"], {
38093
+ await execFileAsync15("qmd", ["embed"], {
37883
38094
  cwd: rootDir,
37884
38095
  timeout: 12e4,
37885
38096
  maxBuffer: 1024 * 1024
@@ -37915,12 +38126,12 @@ async function refreshQmdAgentMemoryIndex(rootDir, agentId, options) {
37915
38126
  }
37916
38127
  }
37917
38128
  const promise = (async () => {
37918
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38129
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37919
38130
  const { agentMemoryWorkspacePath: agentMemoryWorkspacePath2 } = await Promise.resolve().then(() => (init_memory_dreams(), memory_dreams_exports));
37920
38131
  const workspacePath = agentMemoryWorkspacePath2(rootDir, agentId);
37921
38132
  await mkdir6(workspacePath, { recursive: true });
37922
38133
  try {
37923
- 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"], {
37924
38135
  cwd: rootDir,
37925
38136
  timeout: 4e3,
37926
38137
  maxBuffer: 512 * 1024
@@ -37933,8 +38144,8 @@ ${stderr}`)) {
37933
38144
  throw err;
37934
38145
  }
37935
38146
  }
37936
- await execFileAsync14("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
37937
- 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 });
37938
38149
  })();
37939
38150
  qmdAgentRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
37940
38151
  try {
@@ -37955,8 +38166,8 @@ function scheduleQmdAgentMemoryRefresh(rootDir, agentId) {
37955
38166
  }
37956
38167
  async function isQmdAvailable() {
37957
38168
  try {
37958
- const execFileAsync14 = await getDefaultExecFileAsync();
37959
- await execFileAsync14("qmd", ["--help"], {
38169
+ const execFileAsync15 = await getDefaultExecFileAsync();
38170
+ await execFileAsync15("qmd", ["--help"], {
37960
38171
  timeout: 3e3,
37961
38172
  maxBuffer: 128 * 1024
37962
38173
  });
@@ -37966,12 +38177,12 @@ async function isQmdAvailable() {
37966
38177
  }
37967
38178
  }
37968
38179
  async function installQmd(options) {
37969
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38180
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37970
38181
  const [command, ...args] = QMD_INSTALL_COMMAND.split(" ");
37971
38182
  if (!command || args.length === 0) {
37972
38183
  throw new MemoryBackendError("BACKEND_UNAVAILABLE", "qmd install command is not configured", "qmd");
37973
38184
  }
37974
- await execFileAsync14(command, args, {
38185
+ await execFileAsync15(command, args, {
37975
38186
  timeout: 12e4,
37976
38187
  maxBuffer: 1024 * 1024
37977
38188
  });
@@ -39622,7 +39833,7 @@ var init_task_id_integrity = __esm({
39622
39833
  });
39623
39834
 
39624
39835
  // ../core/src/store.ts
39625
- import { EventEmitter as EventEmitter14 } from "node:events";
39836
+ import { EventEmitter as EventEmitter15 } from "node:events";
39626
39837
  import { randomUUID as randomUUID15 } from "node:crypto";
39627
39838
  import { mkdir as mkdir7, readdir as readdir5, readFile as readFile8, writeFile as writeFile6, rename as rename4, unlink as unlink3 } from "node:fs/promises";
39628
39839
  import { join as join19 } from "node:path";
@@ -39954,6 +40165,7 @@ var init_store = __esm({
39954
40165
  init_research_store();
39955
40166
  init_experiment_session_store();
39956
40167
  init_todo_store();
40168
+ init_goal_store();
39957
40169
  init_eval_store();
39958
40170
  init_migration();
39959
40171
  init_central_core();
@@ -40147,7 +40359,7 @@ var init_store = __esm({
40147
40359
  this.name = "HandoffInvariantViolationError";
40148
40360
  }
40149
40361
  };
40150
- TaskStore = class _TaskStore extends EventEmitter14 {
40362
+ TaskStore = class _TaskStore extends EventEmitter15 {
40151
40363
  constructor(rootDir, globalSettingsDir, options) {
40152
40364
  super();
40153
40365
  this.rootDir = rootDir;
@@ -40279,6 +40491,8 @@ var init_store = __esm({
40279
40491
  experimentSessionStore = null;
40280
40492
  /** Cached TodoStore instance */
40281
40493
  todoStore = null;
40494
+ /** Cached GoalStore instance */
40495
+ goalStore = null;
40282
40496
  /** Cached EvalStore instance */
40283
40497
  evalStore = null;
40284
40498
  /** Cached SecretsStore instance */
@@ -42662,9 +42876,9 @@ ${outcome}`;
42662
42876
  if (validation.invalid.length > 0) {
42663
42877
  if (this.isWatching) this.taskCache.delete(id);
42664
42878
  this.deleteTaskById(id);
42665
- const { rm: rm9 } = await import("node:fs/promises");
42879
+ const { rm: rm10 } = await import("node:fs/promises");
42666
42880
  if (existsSync15(dir)) {
42667
- await rm9(dir, { recursive: true, force: true });
42881
+ await rm10(dir, { recursive: true, force: true });
42668
42882
  }
42669
42883
  throw new InvalidFileScopeError(id, validation.invalid);
42670
42884
  }
@@ -42759,10 +42973,10 @@ ${outcome}`;
42759
42973
  });
42760
42974
  if (this.isWatching) this.taskCache.delete(task.id);
42761
42975
  this.deleteTaskById(task.id);
42762
- const { rm: rm9 } = await import("node:fs/promises");
42976
+ const { rm: rm10 } = await import("node:fs/promises");
42763
42977
  const taskDir = this.taskDir(task.id);
42764
42978
  if (existsSync15(taskDir)) {
42765
- await rm9(taskDir, { recursive: true, force: true });
42979
+ await rm10(taskDir, { recursive: true, force: true });
42766
42980
  }
42767
42981
  throw new TombstonedTaskResurrectionError(
42768
42982
  tombstonedMatch.id,
@@ -45779,8 +45993,8 @@ ${newTask.description}
45779
45993
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
45780
45994
  this.db.bumpLastModified();
45781
45995
  });
45782
- const { rm: rm9 } = await import("node:fs/promises");
45783
- await rm9(dir, { recursive: true, force: true });
45996
+ const { rm: rm10 } = await import("node:fs/promises");
45997
+ await rm10(dir, { recursive: true, force: true });
45784
45998
  if (this.isWatching) {
45785
45999
  this.taskCache.delete(id);
45786
46000
  }
@@ -47189,14 +47403,14 @@ ${newTask.description}
47189
47403
  if (rows.length === 0) {
47190
47404
  return;
47191
47405
  }
47192
- const { rm: rm9 } = await import("node:fs/promises");
47406
+ const { rm: rm10 } = await import("node:fs/promises");
47193
47407
  for (const row of rows) {
47194
47408
  const task = this.rowToTask(row);
47195
47409
  const archivedAt = task.columnMovedAt ?? task.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
47196
47410
  const entry = await this.taskToArchiveEntry(task, archivedAt);
47197
47411
  this.archiveDb.upsert(entry);
47198
47412
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
47199
- await rm9(this.taskDir(task.id), { recursive: true, force: true });
47413
+ await rm10(this.taskDir(task.id), { recursive: true, force: true });
47200
47414
  if (this.isWatching) {
47201
47415
  this.taskCache.delete(task.id);
47202
47416
  }
@@ -47223,8 +47437,8 @@ ${newTask.description}
47223
47437
  this.archiveDb.upsert(entry);
47224
47438
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
47225
47439
  this.db.bumpLastModified();
47226
- const { rm: rm9 } = await import("node:fs/promises");
47227
- await rm9(dir, { recursive: true, force: true });
47440
+ const { rm: rm10 } = await import("node:fs/promises");
47441
+ await rm10(dir, { recursive: true, force: true });
47228
47442
  if (this.isWatching) {
47229
47443
  this.taskCache.delete(task.id);
47230
47444
  }
@@ -47958,6 +48172,16 @@ ${notificationsSection}`;
47958
48172
  }
47959
48173
  return this.todoStore;
47960
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
+ }
47961
48185
  /**
47962
48186
  * Get the EvalStore instance for eval run and task result operations.
47963
48187
  * Lazily initializes the EvalStore on first access.
@@ -49155,8 +49379,11 @@ function describeAbortReason(reason) {
49155
49379
  if (typeof reason === "string") return reason;
49156
49380
  return "aborted";
49157
49381
  }
49382
+ function shouldAutoAppendJsonFlag(args) {
49383
+ return args[0] !== "api" && !args.includes("--json");
49384
+ }
49158
49385
  function runGhJson(args, cwd) {
49159
- const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
49386
+ const jsonArgs = shouldAutoAppendJsonFlag(args) ? [...args, "--json"] : args;
49160
49387
  const output = runGh(jsonArgs, cwd);
49161
49388
  try {
49162
49389
  return JSON.parse(output);
@@ -49165,7 +49392,7 @@ function runGhJson(args, cwd) {
49165
49392
  }
49166
49393
  }
49167
49394
  async function runGhJsonAsync(args, cwdOrOptions) {
49168
- const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
49395
+ const jsonArgs = shouldAutoAppendJsonFlag(args) ? [...args, "--json"] : args;
49169
49396
  const output = await runGhAsync(jsonArgs, cwdOrOptions);
49170
49397
  try {
49171
49398
  return JSON.parse(output);
@@ -49765,7 +49992,7 @@ var routine_store_exports = {};
49765
49992
  __export(routine_store_exports, {
49766
49993
  RoutineStore: () => RoutineStore
49767
49994
  });
49768
- import { EventEmitter as EventEmitter15 } from "node:events";
49995
+ import { EventEmitter as EventEmitter16 } from "node:events";
49769
49996
  import { randomUUID as randomUUID16 } from "node:crypto";
49770
49997
  var import_cron_parser2, CRON_TIMEZONE2, RoutineStore;
49771
49998
  var init_routine_store = __esm({
@@ -49776,7 +50003,7 @@ var init_routine_store = __esm({
49776
50003
  init_routine();
49777
50004
  init_project_root_guard();
49778
50005
  CRON_TIMEZONE2 = "UTC";
49779
- RoutineStore = class _RoutineStore extends EventEmitter15 {
50006
+ RoutineStore = class _RoutineStore extends EventEmitter16 {
49780
50007
  constructor(rootDir, options) {
49781
50008
  super();
49782
50009
  this.rootDir = rootDir;
@@ -50378,7 +50605,7 @@ import { basename as basename8, dirname as dirname7, extname, isAbsolute as isAb
50378
50605
  import { stat as stat3 } from "node:fs/promises";
50379
50606
  import { copyFile } from "node:fs/promises";
50380
50607
  import { pathToFileURL } from "node:url";
50381
- import { EventEmitter as EventEmitter16 } from "node:events";
50608
+ import { EventEmitter as EventEmitter17 } from "node:events";
50382
50609
  var MINIMUM_FUSION_VERSION, moduleImportVersion, PluginLoader;
50383
50610
  var init_plugin_loader = __esm({
50384
50611
  "../core/src/plugin-loader.ts"() {
@@ -50389,7 +50616,7 @@ var init_plugin_loader = __esm({
50389
50616
  init_plugin_security_scan();
50390
50617
  MINIMUM_FUSION_VERSION = "0.1.0";
50391
50618
  moduleImportVersion = 0;
50392
- PluginLoader = class extends EventEmitter16 {
50619
+ PluginLoader = class extends EventEmitter17 {
50393
50620
  constructor(options) {
50394
50621
  super();
50395
50622
  this.options = options;
@@ -55941,15 +56168,15 @@ var require_fd_slicer = __commonJS({
55941
56168
  var Writable = stream.Writable;
55942
56169
  var PassThrough = stream.PassThrough;
55943
56170
  var Pend = require_pend();
55944
- var EventEmitter34 = __require("events").EventEmitter;
56171
+ var EventEmitter35 = __require("events").EventEmitter;
55945
56172
  exports.createFromBuffer = createFromBuffer;
55946
56173
  exports.createFromFd = createFromFd;
55947
56174
  exports.BufferSlicer = BufferSlicer;
55948
56175
  exports.FdSlicer = FdSlicer;
55949
- util.inherits(FdSlicer, EventEmitter34);
56176
+ util.inherits(FdSlicer, EventEmitter35);
55950
56177
  function FdSlicer(fd, options) {
55951
56178
  options = options || {};
55952
- EventEmitter34.call(this);
56179
+ EventEmitter35.call(this);
55953
56180
  this.fd = fd;
55954
56181
  this.pend = new Pend();
55955
56182
  this.pend.max = 1;
@@ -56093,9 +56320,9 @@ var require_fd_slicer = __commonJS({
56093
56320
  this.destroyed = true;
56094
56321
  this.context.unref();
56095
56322
  };
56096
- util.inherits(BufferSlicer, EventEmitter34);
56323
+ util.inherits(BufferSlicer, EventEmitter35);
56097
56324
  function BufferSlicer(buffer, options) {
56098
- EventEmitter34.call(this);
56325
+ EventEmitter35.call(this);
56099
56326
  options = options || {};
56100
56327
  this.refCount = 0;
56101
56328
  this.buffer = buffer;
@@ -56507,7 +56734,7 @@ var require_yauzl = __commonJS({
56507
56734
  var fd_slicer = require_fd_slicer();
56508
56735
  var crc32 = require_buffer_crc32();
56509
56736
  var util = __require("util");
56510
- var EventEmitter34 = __require("events").EventEmitter;
56737
+ var EventEmitter35 = __require("events").EventEmitter;
56511
56738
  var Transform = __require("stream").Transform;
56512
56739
  var PassThrough = __require("stream").PassThrough;
56513
56740
  var Writable = __require("stream").Writable;
@@ -56639,10 +56866,10 @@ var require_yauzl = __commonJS({
56639
56866
  callback(new Error("end of central directory record signature not found"));
56640
56867
  });
56641
56868
  }
56642
- util.inherits(ZipFile, EventEmitter34);
56869
+ util.inherits(ZipFile, EventEmitter35);
56643
56870
  function ZipFile(reader, centralDirectoryOffset, fileSize, entryCount, comment, autoClose, lazyEntries, decodeStrings, validateEntrySizes, strictFileNames) {
56644
56871
  var self = this;
56645
- EventEmitter34.call(self);
56872
+ EventEmitter35.call(self);
56646
56873
  self.reader = reader;
56647
56874
  self.reader.on("error", function(err) {
56648
56875
  emitError(self, err);
@@ -57003,9 +57230,9 @@ var require_yauzl = __commonJS({
57003
57230
  }
57004
57231
  cb();
57005
57232
  };
57006
- util.inherits(RandomAccessReader, EventEmitter34);
57233
+ util.inherits(RandomAccessReader, EventEmitter35);
57007
57234
  function RandomAccessReader() {
57008
- EventEmitter34.call(this);
57235
+ EventEmitter35.call(this);
57009
57236
  this.refCount = 0;
57010
57237
  }
57011
57238
  RandomAccessReader.prototype.ref = function() {
@@ -57136,11 +57363,11 @@ var require_extract_zip = __commonJS({
57136
57363
  var { createWriteStream, promises: fs6 } = __require("fs");
57137
57364
  var getStream = require_get_stream();
57138
57365
  var path6 = __require("path");
57139
- var { promisify: promisify50 } = __require("util");
57366
+ var { promisify: promisify51 } = __require("util");
57140
57367
  var stream = __require("stream");
57141
57368
  var yauzl = require_yauzl();
57142
- var openZip = promisify50(yauzl.open);
57143
- var pipeline = promisify50(stream.pipeline);
57369
+ var openZip = promisify51(yauzl.open);
57370
+ var pipeline = promisify51(stream.pipeline);
57144
57371
  var Extractor = class {
57145
57372
  constructor(zipPath, opts) {
57146
57373
  this.zipPath = zipPath;
@@ -57222,7 +57449,7 @@ var require_extract_zip = __commonJS({
57222
57449
  await fs6.mkdir(destDir, mkdirOptions);
57223
57450
  if (isDir) return;
57224
57451
  debug("opening read stream", dest);
57225
- const readStream = await promisify50(this.zipfile.openReadStream.bind(this.zipfile))(entry);
57452
+ const readStream = await promisify51(this.zipfile.openReadStream.bind(this.zipfile))(entry);
57226
57453
  if (symlink) {
57227
57454
  const link = await getStream(readStream);
57228
57455
  debug("creating symlink", link, dest);
@@ -68243,14 +68470,14 @@ var init_agent_companies_exporter = __esm({
68243
68470
  });
68244
68471
 
68245
68472
  // ../core/src/chat-store.ts
68246
- import { EventEmitter as EventEmitter17 } from "node:events";
68473
+ import { EventEmitter as EventEmitter18 } from "node:events";
68247
68474
  import { randomUUID as randomUUID19 } from "node:crypto";
68248
68475
  var ChatStore;
68249
68476
  var init_chat_store = __esm({
68250
68477
  "../core/src/chat-store.ts"() {
68251
68478
  "use strict";
68252
68479
  init_db();
68253
- ChatStore = class extends EventEmitter17 {
68480
+ ChatStore = class extends EventEmitter18 {
68254
68481
  constructor(fusionDir, db) {
68255
68482
  super();
68256
68483
  this.fusionDir = fusionDir;
@@ -68622,7 +68849,7 @@ var init_chat_store = __esm({
68622
68849
  *
68623
68850
  * @param sessionId - Session ID
68624
68851
  * @param filter - Optional filter (limit, offset, before cursor)
68625
- * @returns Array of messages ordered by createdAt ASC
68852
+ * @returns Array of messages ordered by createdAt ASC (default) or DESC
68626
68853
  */
68627
68854
  getMessages(sessionId, filter) {
68628
68855
  const whereClauses = ["sessionId = ?"];
@@ -68634,10 +68861,11 @@ var init_chat_store = __esm({
68634
68861
  const whereSql = whereClauses.join(" AND ");
68635
68862
  const limit = filter?.limit ?? 100;
68636
68863
  const offset = filter?.offset ?? 0;
68864
+ const order = filter?.order === "desc" ? "DESC" : "ASC";
68637
68865
  const rows = this.db.prepare(`
68638
68866
  SELECT * FROM chat_messages
68639
68867
  WHERE ${whereSql}
68640
- ORDER BY createdAt ASC
68868
+ ORDER BY createdAt ${order}
68641
68869
  LIMIT ? OFFSET ?
68642
68870
  `).all(...params, limit, offset);
68643
68871
  return rows.map((row) => this.rowToMessage(row));
@@ -69341,6 +69569,7 @@ var init_secrets_sync = __esm({
69341
69569
  // ../core/src/index.ts
69342
69570
  var src_exports = {};
69343
69571
  __export(src_exports, {
69572
+ ACTIVE_GOAL_LIMIT: () => ACTIVE_GOAL_LIMIT,
69344
69573
  AGENT_PERMISSIONS: () => AGENT_PERMISSIONS,
69345
69574
  AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: () => AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
69346
69575
  AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES,
@@ -69354,6 +69583,7 @@ __export(src_exports, {
69354
69583
  AUTOPILOT_STATES: () => AUTOPILOT_STATES,
69355
69584
  AUTO_RECOVERY_MODES: () => AUTO_RECOVERY_MODES,
69356
69585
  AUTO_SUMMARIZE_SCHEDULE_NAME: () => AUTO_SUMMARIZE_SCHEDULE_NAME,
69586
+ ActiveGoalLimitExceededError: () => ActiveGoalLimitExceededError,
69357
69587
  AgentCompaniesParseError: () => AgentCompaniesParseError,
69358
69588
  AgentStore: () => AgentStore,
69359
69589
  AiServiceError: () => AiServiceError,
@@ -69447,6 +69677,7 @@ __export(src_exports, {
69447
69677
  FirstRunDetector: () => FirstRunDetector,
69448
69678
  GLOBAL_SETTINGS_KEYS: () => GLOBAL_SETTINGS_KEYS,
69449
69679
  GlobalSettingsStore: () => GlobalSettingsStore,
69680
+ GoalStore: () => GoalStore,
69450
69681
  HIGH_FANOUT_BLOCKER_TODO_THRESHOLD: () => HIGH_FANOUT_BLOCKER_TODO_THRESHOLD,
69451
69682
  HandoffInvariantViolationError: () => HandoffInvariantViolationError,
69452
69683
  INSIGHT_EXTRACTION_SCHEDULE_NAME: () => INSIGHT_EXTRACTION_SCHEDULE_NAME,
@@ -69481,6 +69712,7 @@ __export(src_exports, {
69481
69712
  MEMORY_LONG_TERM_FILENAME: () => MEMORY_LONG_TERM_FILENAME,
69482
69713
  MEMORY_WORKING_PATH: () => MEMORY_WORKING_PATH,
69483
69714
  MEMORY_WORKSPACE_PATH: () => MEMORY_WORKSPACE_PATH,
69715
+ MERGER_MODES: () => MERGER_MODES,
69484
69716
  MERGE_ADVANCE_AUTO_SYNC_MODES: () => MERGE_ADVANCE_AUTO_SYNC_MODES,
69485
69717
  MERGE_AUDIT_AUTO_RECOVERY_MODES: () => MERGE_AUDIT_AUTO_RECOVERY_MODES,
69486
69718
  MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT: () => MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT,
@@ -69670,6 +69902,7 @@ __export(src_exports, {
69670
69902
  createSecretCipher: () => createSecretCipher,
69671
69903
  createTaskMetadataSnapshot: () => createTaskMetadataSnapshot,
69672
69904
  currentBackupTimestamp: () => currentBackupTimestamp,
69905
+ customProviderRegistryKey: () => customProviderRegistryKey,
69673
69906
  dailyMemoryPath: () => dailyMemoryPath,
69674
69907
  detectDependencyCycle: () => detectDependencyCycle,
69675
69908
  detectFnBinary: () => detectFnBinary,
@@ -69827,6 +70060,7 @@ __export(src_exports, {
69827
70060
  normalizeMergeConflictStrategy: () => normalizeMergeConflictStrategy,
69828
70061
  normalizeMergeIntegrationWorktreeMode: () => normalizeMergeIntegrationWorktreeMode,
69829
70062
  normalizeMergeStrategyOverlapBehavior: () => normalizeMergeStrategyOverlapBehavior,
70063
+ normalizeMergerMode: () => normalizeMergerMode,
69830
70064
  normalizeMessageParticipant: () => normalizeMessageParticipant,
69831
70065
  normalizePermissions: () => normalizePermissions,
69832
70066
  normalizePluginUiContributionDefinition: () => normalizePluginUiContributionDefinition,
@@ -69995,6 +70229,7 @@ var init_src = __esm({
69995
70229
  "use strict";
69996
70230
  init_types();
69997
70231
  init_types();
70232
+ init_custom_provider_key();
69998
70233
  init_mock_provider_constants();
69999
70234
  init_worktrunk_settings();
70000
70235
  init_agent_memory_mode();
@@ -70083,6 +70318,8 @@ var init_src = __esm({
70083
70318
  init_task_priority();
70084
70319
  init_mission_types();
70085
70320
  init_mission_store();
70321
+ init_goal_types();
70322
+ init_goal_store();
70086
70323
  init_central_core();
70087
70324
  init_central_db();
70088
70325
  init_node_connection();
@@ -70713,15 +70950,15 @@ function evaluateAgentActionGate(params) {
70713
70950
  let resourceId;
70714
70951
  if (params.toolName === "bash") {
70715
70952
  const command = extractShellCommand(args);
70716
- const git2 = classifyGitCommand(command);
70717
- if (git2?.write) {
70953
+ const git3 = classifyGitCommand(command);
70954
+ if (git3?.write) {
70718
70955
  category = "git_write";
70719
- operation = git2.operation;
70956
+ operation = git3.operation;
70720
70957
  resourceType = "git";
70721
70958
  } else {
70722
70959
  category = "command_execution";
70723
- operation = git2?.operation ?? "shell command";
70724
- resourceType = git2 ? "git" : "command";
70960
+ operation = git3?.operation ?? "shell command";
70961
+ resourceType = git3 ? "git" : "command";
70725
70962
  }
70726
70963
  } else if (params.toolName === "write" || params.toolName === "edit") {
70727
70964
  category = "file_write_delete";
@@ -71961,8 +72198,8 @@ var init_context_limit_detector = __esm({
71961
72198
  import { existsSync as existsSync24, readFileSync as readFileSync8 } from "node:fs";
71962
72199
  import { homedir as homedir5 } from "node:os";
71963
72200
  import { join as join29 } from "node:path";
71964
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
71965
- import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
72201
+ import { AuthStorage } from "@earendil-works/pi-coding-agent";
72202
+ import { getOAuthProvider } from "@earendil-works/pi-ai/oauth";
71966
72203
  function getHomeDir5() {
71967
72204
  return process.env.HOME || process.env.USERPROFILE || homedir5();
71968
72205
  }
@@ -72349,7 +72586,7 @@ var init_workflow_step_tool_policy = __esm({
72349
72586
 
72350
72587
  // ../engine/src/pi.ts
72351
72588
  import { existsSync as existsSync25, readFileSync as readFileSync10 } from "node:fs";
72352
- import { exec as exec2 } from "node:child_process";
72589
+ import { exec as exec2, execFile as execFile3 } from "node:child_process";
72353
72590
  import { promisify as promisify3 } from "node:util";
72354
72591
  import { createRequire as createRequire3 } from "node:module";
72355
72592
  import { basename as basename9, dirname as dirname11, join as join31, relative as relative4, isAbsolute as isAbsolute8, resolve as resolve15 } from "node:path";
@@ -72371,7 +72608,61 @@ import {
72371
72608
  ModelRegistry,
72372
72609
  SessionManager,
72373
72610
  SettingsManager
72374
- } from "@mariozechner/pi-coding-agent";
72611
+ } from "@earendil-works/pi-coding-agent";
72612
+ function normalizeRtkRewriteOptions(options) {
72613
+ const modeEnv = process.env.FUSION_RTK_REWRITE?.toLowerCase();
72614
+ const envMode = modeEnv === "1" || modeEnv === "true" || modeEnv === "rewrite" ? "rewrite" : "off";
72615
+ const envTimeoutMs = Number.parseInt(process.env.FUSION_RTK_REWRITE_TIMEOUT_MS ?? "2000", 10);
72616
+ const requestedTimeoutMs = options?.timeoutMs ?? envTimeoutMs;
72617
+ return {
72618
+ mode: options?.mode ?? envMode,
72619
+ timeoutMs: Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 ? requestedTimeoutMs : 2e3
72620
+ };
72621
+ }
72622
+ function resolveRtkRewriteOptions() {
72623
+ return normalizeRtkRewriteOptions();
72624
+ }
72625
+ function getRtkErrorCode(error) {
72626
+ if (!error) return 0;
72627
+ const rawCode = error.code;
72628
+ if (typeof rawCode === "number" || typeof rawCode === "string") return rawCode;
72629
+ return null;
72630
+ }
72631
+ function shouldWarnForRtkFailure(code) {
72632
+ if (typeof code === "number") return !RTK_EXPECTED_PASSTHROUGH_EXIT_CODES.has(code);
72633
+ if (typeof code === "string") return !RTK_EXPECTED_FAIL_OPEN_ERROR_CODES.has(code);
72634
+ return true;
72635
+ }
72636
+ function rewriteCommandWithRtk(command, options, signal) {
72637
+ if (signal?.aborted) {
72638
+ return Promise.resolve(null);
72639
+ }
72640
+ return new Promise((resolve32) => {
72641
+ execFile3(
72642
+ "rtk",
72643
+ ["rewrite", command],
72644
+ {
72645
+ timeout: options.timeoutMs,
72646
+ maxBuffer: RTK_REWRITE_MAX_BUFFER_BYTES,
72647
+ signal,
72648
+ windowsHide: true
72649
+ },
72650
+ (error, stdout, stderr) => {
72651
+ const code = getRtkErrorCode(error);
72652
+ if (typeof code !== "number" || !RTK_ACCEPTED_REWRITE_EXIT_CODES.has(code)) {
72653
+ if (shouldWarnForRtkFailure(code)) {
72654
+ const reason = stderr?.toString().trim() || (error instanceof Error ? error.message : `exit ${String(code)}`);
72655
+ piLog.warn(`[pi] rtk rewrite failed open: ${reason}`);
72656
+ }
72657
+ resolve32(null);
72658
+ return;
72659
+ }
72660
+ const rewritten = stdout.toString().trim();
72661
+ resolve32(rewritten && rewritten !== command ? rewritten : null);
72662
+ }
72663
+ );
72664
+ });
72665
+ }
72375
72666
  function setHostExtensionPaths(paths) {
72376
72667
  hostExtensionPaths = [...paths];
72377
72668
  }
@@ -73272,6 +73563,36 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
73272
73563
  };
73273
73564
  });
73274
73565
  }
73566
+ function wrapToolsWithRtkRewrite(tools, options = resolveRtkRewriteOptions()) {
73567
+ const resolvedOptions = normalizeRtkRewriteOptions(options);
73568
+ if (resolvedOptions.mode !== "rewrite") {
73569
+ return tools;
73570
+ }
73571
+ return tools.map((tool) => {
73572
+ if (tool.name !== "bash") {
73573
+ return tool;
73574
+ }
73575
+ const originalExecute = tool.execute;
73576
+ return {
73577
+ ...tool,
73578
+ execute: async (...args) => {
73579
+ const params = args[1];
73580
+ const command = params?.command;
73581
+ if (typeof command !== "string" || !command.trim()) {
73582
+ return originalExecute(...args);
73583
+ }
73584
+ const signal = args[2];
73585
+ const rewrittenCommand = await rewriteCommandWithRtk(command, resolvedOptions, signal);
73586
+ if (!rewrittenCommand) {
73587
+ return originalExecute(...args);
73588
+ }
73589
+ const rewrittenArgs = [...args];
73590
+ rewrittenArgs[1] = { ...params ?? {}, command: rewrittenCommand };
73591
+ return originalExecute(...rewrittenArgs);
73592
+ }
73593
+ };
73594
+ });
73595
+ }
73275
73596
  function wrapToolsWithPermanentAgentGating(tools, gating) {
73276
73597
  if (!gating) {
73277
73598
  return tools;
@@ -73407,11 +73728,13 @@ async function createFnAgent2(options) {
73407
73728
  const modelRegistry = ModelRegistry.create(authStorage, getModelRegistryModelsPath());
73408
73729
  const resolvedProjectRoot = getProjectRootFromWorktree(options.cwd) ?? resolvePiExtensionProjectRoot(options.cwd);
73409
73730
  await registerExtensionProviders(resolvedProjectRoot, modelRegistry);
73410
- for (const provider of readCustomProviders()) {
73731
+ const customProviders = readCustomProviders();
73732
+ for (const provider of customProviders) {
73411
73733
  try {
73412
- modelRegistry.registerProvider(provider.id, {
73734
+ const registryKey2 = customProviderRegistryKey(provider, customProviders);
73735
+ modelRegistry.registerProvider(registryKey2, {
73413
73736
  baseUrl: provider.baseUrl,
73414
- api: provider.apiType === "anthropic-compatible" ? "anthropic" : "openai-completions",
73737
+ api: provider.apiType === "anthropic-compatible" ? "anthropic" : provider.apiType === "openai-responses" ? "openai-responses" : "openai-completions",
73415
73738
  apiKey: provider.apiKey,
73416
73739
  models: (provider.models ?? []).map((model) => ({
73417
73740
  id: model.id,
@@ -73428,10 +73751,11 @@ async function createFnAgent2(options) {
73428
73751
  maxTokens: 16384
73429
73752
  }))
73430
73753
  });
73431
- piLog.log(`Registered custom provider ${provider.id}`);
73754
+ piLog.log(`Registered custom provider "${provider.name}" (key=${registryKey2}, id=${provider.id})`);
73432
73755
  } catch (error) {
73433
73756
  const message = error instanceof Error ? error.message : String(error);
73434
- piLog.warn(`Failed to register custom provider ${provider.id}: ${message}`);
73757
+ const registryKey2 = customProviderRegistryKey(provider, customProviders);
73758
+ piLog.warn(`Failed to register custom provider "${provider.name}" (key=${registryKey2}, id=${provider.id}): ${message}`);
73435
73759
  }
73436
73760
  }
73437
73761
  modelRegistry.refresh();
@@ -73550,8 +73874,9 @@ async function createFnAgent2(options) {
73550
73874
  ...tools,
73551
73875
  ...readonlyFilteredCustomTools.allowed
73552
73876
  ];
73877
+ const toolsWithRtkRewrite = wrapToolsWithRtkRewrite(toolChainStart);
73553
73878
  const toolsWithPermanentGating = wrapToolsWithPermanentAgentGating(
73554
- toolChainStart,
73879
+ toolsWithRtkRewrite,
73555
73880
  options.permanentAgentGating
73556
73881
  );
73557
73882
  const toolsWithActionGate = wrapToolsWithActionGate(
@@ -73818,7 +74143,7 @@ async function createFnAgent2(options) {
73818
74143
  });
73819
74144
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
73820
74145
  }
73821
- var execAsync, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, promptWithFallbackInFlight, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_TASK_PROMPT_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
74146
+ var execAsync, RTK_ACCEPTED_REWRITE_EXIT_CODES, RTK_EXPECTED_PASSTHROUGH_EXIT_CODES, RTK_EXPECTED_FAIL_OPEN_ERROR_CODES, RTK_REWRITE_MAX_BUFFER_BYTES, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, promptWithFallbackInFlight, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_TASK_PROMPT_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
73822
74147
  var init_pi = __esm({
73823
74148
  "../engine/src/pi.ts"() {
73824
74149
  "use strict";
@@ -73832,6 +74157,10 @@ var init_pi = __esm({
73832
74157
  init_permanent_agent_gating();
73833
74158
  init_workflow_step_tool_policy();
73834
74159
  execAsync = promisify3(exec2);
74160
+ RTK_ACCEPTED_REWRITE_EXIT_CODES = /* @__PURE__ */ new Set([0, 3]);
74161
+ RTK_EXPECTED_PASSTHROUGH_EXIT_CODES = /* @__PURE__ */ new Set([1, 2]);
74162
+ RTK_EXPECTED_FAIL_OPEN_ERROR_CODES = /* @__PURE__ */ new Set(["ABORT_ERR", "ENOENT", "ETIMEDOUT"]);
74163
+ RTK_REWRITE_MAX_BUFFER_BYTES = 64 * 1024;
73835
74164
  hostExtensionPaths = [];
73836
74165
  FN_MEMORY_APPEND_TOOL_NAME = "fn_memory_append";
73837
74166
  FUSION_SHUTDOWN_WRAP_FLAG = "__fusionSessionShutdownDisposeWrapped";
@@ -74809,9 +75138,9 @@ var init_web_search_provider = __esm({
74809
75138
  if (!isRetryableHttp || attempt >= RETRY_MAX_ATTEMPTS) {
74810
75139
  break;
74811
75140
  }
74812
- const delay4 = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
74813
- const jitter = delay4 * (Math.random() * 0.2 - 0.1);
74814
- const totalDelay = Math.max(0, delay4 + jitter);
75141
+ const delay5 = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
75142
+ const jitter = delay5 * (Math.random() * 0.2 - 0.1);
75143
+ const totalDelay = Math.max(0, delay5 + jitter);
74815
75144
  log9.warn(`retrying ${backend} search`, { attempt, totalDelay });
74816
75145
  await sleep2(totalDelay, signal);
74817
75146
  }
@@ -75265,7 +75594,7 @@ import { appendFile as appendFile3, mkdir as mkdir12, readFile as readFile14, re
75265
75594
  import { existsSync as existsSync26 } from "node:fs";
75266
75595
  import { createHash as createHash8 } from "node:crypto";
75267
75596
  import { join as join33, relative as relative6, resolve as resolve17 } from "node:path";
75268
- import { Type } from "@mariozechner/pi-ai";
75597
+ import { Type } from "@earendil-works/pi-ai";
75269
75598
  function sanitizeAgentMemoryId(agentId) {
75270
75599
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
75271
75600
  }
@@ -75435,11 +75764,11 @@ async function refreshAgentMemoryQmdIndex(rootDir, agentMemory) {
75435
75764
  return;
75436
75765
  }
75437
75766
  const promise = (async () => {
75438
- const { execFile: execFile18 } = await import("node:child_process");
75439
- const { promisify: promisify50 } = await import("node:util");
75440
- const execFileAsync14 = promisify50(execFile18);
75767
+ const { execFile: execFile20 } = await import("node:child_process");
75768
+ const { promisify: promisify51 } = await import("node:util");
75769
+ const execFileAsync15 = promisify51(execFile20);
75441
75770
  try {
75442
- await execFileAsync14("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
75771
+ await execFileAsync15("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
75443
75772
  cwd: rootDir,
75444
75773
  timeout: 4e3,
75445
75774
  maxBuffer: 512 * 1024
@@ -75452,8 +75781,8 @@ ${stderr}`)) {
75452
75781
  throw error;
75453
75782
  }
75454
75783
  }
75455
- await execFileAsync14("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
75456
- await execFileAsync14("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
75784
+ await execFileAsync15("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
75785
+ await execFileAsync15("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
75457
75786
  })();
75458
75787
  agentQmdRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
75459
75788
  try {
@@ -75508,10 +75837,10 @@ async function searchAgentMemoryWithQmd(rootDir, agentMemory, query, limit) {
75508
75837
  }
75509
75838
  try {
75510
75839
  await refreshAgentMemoryQmdIndex(rootDir, agentMemory);
75511
- const { execFile: execFile18 } = await import("node:child_process");
75512
- const { promisify: promisify50 } = await import("node:util");
75513
- const execFileAsync14 = promisify50(execFile18);
75514
- const { stdout } = await execFileAsync14("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
75840
+ const { execFile: execFile20 } = await import("node:child_process");
75841
+ const { promisify: promisify51 } = await import("node:util");
75842
+ const execFileAsync15 = promisify51(execFile20);
75843
+ const { stdout } = await execFileAsync15("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
75515
75844
  cwd: rootDir,
75516
75845
  timeout: 4e3,
75517
75846
  maxBuffer: 1024 * 1024
@@ -79015,6 +79344,24 @@ var init_webhook_provider = __esm({
79015
79344
  }
79016
79345
  });
79017
79346
 
79347
+ // ../engine/src/transient-merge-error-classifier.ts
79348
+ function classifyTransientMergeError(error) {
79349
+ if (!error) return null;
79350
+ if (/lease-handoff-failed[^a-z]+target-not-queued/i.test(error)) {
79351
+ return "lease-handoff-target-not-queued";
79352
+ }
79353
+ const sameSha = error.match(/advanced concurrently \(expected ([0-9a-f]{7,40}),\s+observed ([0-9a-f]{7,40})\)/i);
79354
+ if (sameSha && sameSha[1].toLowerCase() === sameSha[2].toLowerCase()) {
79355
+ return "spurious-concurrent-advance-same-sha";
79356
+ }
79357
+ return null;
79358
+ }
79359
+ var init_transient_merge_error_classifier = __esm({
79360
+ "../engine/src/transient-merge-error-classifier.ts"() {
79361
+ "use strict";
79362
+ }
79363
+ });
79364
+
79018
79365
  // ../engine/src/notification/notification-service.ts
79019
79366
  var NotificationService;
79020
79367
  var init_notification_service = __esm({
@@ -79023,6 +79370,7 @@ var init_notification_service = __esm({
79023
79370
  init_src();
79024
79371
  init_notifier();
79025
79372
  init_logger2();
79373
+ init_transient_merge_error_classifier();
79026
79374
  init_ntfy_provider();
79027
79375
  init_webhook_provider();
79028
79376
  NotificationService = class {
@@ -79124,6 +79472,14 @@ var init_notification_service = __esm({
79124
79472
  return;
79125
79473
  }
79126
79474
  if (task.status === "failed") {
79475
+ const transientClass = classifyTransientMergeError(task.error);
79476
+ if (transientClass) {
79477
+ this.failureNotificationSuppressedCount += 1;
79478
+ schedulerLog.log(
79479
+ `[notify] ${task.id} transient merge failure (${transientClass}) \u2014 suppressed notification (self-heal in flight)`
79480
+ );
79481
+ return;
79482
+ }
79127
79483
  if (this.failureNotificationMode === "all") {
79128
79484
  this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
79129
79485
  } else {
@@ -79440,6 +79796,14 @@ var init_notification_service = __esm({
79440
79796
  schedulerLog.log(`[notify] ${taskId} no longer failed at dispatch time \u2014 suppressed notification`);
79441
79797
  return;
79442
79798
  }
79799
+ const transientClassAtDispatch = classifyTransientMergeError(task.error);
79800
+ if (transientClassAtDispatch) {
79801
+ this.failureNotificationSuppressedCount += 1;
79802
+ schedulerLog.log(
79803
+ `[notify] ${taskId} transient merge failure (${transientClassAtDispatch}) at dispatch time \u2014 suppressed notification (self-heal in flight)`
79804
+ );
79805
+ return;
79806
+ }
79443
79807
  const isTerminal = task.paused === true || task.column === "in-review";
79444
79808
  if (this.failureNotificationMode === "terminal-only" && !isTerminal) {
79445
79809
  this.failureNotificationSuppressedCount += 1;
@@ -79566,6 +79930,69 @@ var init_oauth_expiry_monitor = __esm({
79566
79930
  }
79567
79931
  });
79568
79932
 
79933
+ // ../engine/src/notification/oauth-validity-logger.ts
79934
+ var DEFAULT_INTERVAL_MS2, OAuthValidityLogger;
79935
+ var init_oauth_validity_logger = __esm({
79936
+ "../engine/src/notification/oauth-validity-logger.ts"() {
79937
+ "use strict";
79938
+ init_logger2();
79939
+ DEFAULT_INTERVAL_MS2 = 24 * 60 * 60 * 1e3;
79940
+ OAuthValidityLogger = class {
79941
+ constructor(opts) {
79942
+ this.opts = opts;
79943
+ this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
79944
+ this.clock = opts.clock ?? Date.now;
79945
+ this.logger = opts.logger ?? ((message, meta) => schedulerLog.warn(message, meta));
79946
+ }
79947
+ intervalMs;
79948
+ clock;
79949
+ logger;
79950
+ timer = null;
79951
+ async start() {
79952
+ if (this.timer) {
79953
+ return;
79954
+ }
79955
+ await this.check();
79956
+ this.timer = setInterval(() => {
79957
+ void this.check();
79958
+ }, this.intervalMs);
79959
+ this.timer.unref?.();
79960
+ }
79961
+ stop() {
79962
+ if (!this.timer) {
79963
+ return;
79964
+ }
79965
+ clearInterval(this.timer);
79966
+ this.timer = null;
79967
+ }
79968
+ async check() {
79969
+ this.opts.authStorage.reload?.();
79970
+ const providers = this.opts.authStorage.getOAuthProviders?.() ?? [];
79971
+ const now = this.clock();
79972
+ for (const provider of providers) {
79973
+ try {
79974
+ const credential = this.opts.authStorage.get?.(provider.id);
79975
+ if (credential?.type !== "oauth" || typeof credential.expires !== "number") {
79976
+ continue;
79977
+ }
79978
+ if (credential.expires > now) {
79979
+ continue;
79980
+ }
79981
+ this.logger("oauth credential expired \u2014 provider re-login required", {
79982
+ providerId: provider.id,
79983
+ providerName: provider.name,
79984
+ expiresAt: new Date(credential.expires).toISOString()
79985
+ });
79986
+ } catch (error) {
79987
+ const message = error instanceof Error ? error.message : String(error);
79988
+ schedulerLog.warn(`OAuth validity logger failed for provider=${provider.id}: ${message}`);
79989
+ }
79990
+ }
79991
+ }
79992
+ };
79993
+ }
79994
+ });
79995
+
79569
79996
  // ../engine/src/notification/index.ts
79570
79997
  var init_notification = __esm({
79571
79998
  "../engine/src/notification/index.ts"() {
@@ -79574,6 +80001,7 @@ var init_notification = __esm({
79574
80001
  init_webhook_provider();
79575
80002
  init_notification_service();
79576
80003
  init_oauth_expiry_monitor();
80004
+ init_oauth_validity_logger();
79577
80005
  }
79578
80006
  });
79579
80007
 
@@ -79628,6 +80056,19 @@ function truncateNtfyMessage(message) {
79628
80056
  }
79629
80057
  return best;
79630
80058
  }
80059
+ function ntfyPriorityToInt(priority) {
80060
+ switch (priority) {
80061
+ case "low":
80062
+ return 2;
80063
+ case "high":
80064
+ return 4;
80065
+ case "urgent":
80066
+ return 5;
80067
+ case "default":
80068
+ default:
80069
+ return 3;
80070
+ }
80071
+ }
79631
80072
  function resolveNtfyEvents(events) {
79632
80073
  return events ? [...events] : [...DEFAULT_NTFY_EVENTS];
79633
80074
  }
@@ -79677,10 +80118,10 @@ async function sendNtfyNotificationWithResult({
79677
80118
  const truncatedMessage = truncateNtfyMessage(message);
79678
80119
  const latin1Safe = isLatin1Safe(truncatedTitle) && isLatin1Safe(truncatedMessage);
79679
80120
  const headers = {
79680
- Priority: priority,
79681
80121
  "Content-Type": latin1Safe ? "text/plain" : "application/json"
79682
80122
  };
79683
80123
  if (latin1Safe) {
80124
+ headers.Priority = priority;
79684
80125
  headers.Title = truncatedTitle;
79685
80126
  if (clickUrl) {
79686
80127
  headers.Click = clickUrl;
@@ -79696,7 +80137,7 @@ async function sendNtfyNotificationWithResult({
79696
80137
  topic,
79697
80138
  title: truncatedTitle,
79698
80139
  message: truncatedMessage,
79699
- priority,
80140
+ priority: ntfyPriorityToInt(priority),
79700
80141
  ...clickUrl ? { click: clickUrl } : {}
79701
80142
  }),
79702
80143
  signal
@@ -83389,7 +83830,7 @@ ${getErrorStderr(error) ?? ""}`;
83389
83830
  import { createHash as createHash9 } from "node:crypto";
83390
83831
  import { promises as fs5 } from "node:fs";
83391
83832
  import path2 from "node:path";
83392
- import { execFile as execFile3 } from "node:child_process";
83833
+ import { execFile as execFile4 } from "node:child_process";
83393
83834
  function isValidFilename(filename) {
83394
83835
  return !!filename && !filename.includes("/") && !filename.includes("\\") && !filename.includes("..") && !filename.includes("\0") && filename !== FINGERPRINT_FILE;
83395
83836
  }
@@ -83450,7 +83891,7 @@ async function writeSecretsEnvFile(opts) {
83450
83891
  } catch {
83451
83892
  }
83452
83893
  if (cfg?.requireGitignored !== false) {
83453
- const check = await checkIgnored(opts.execFileImpl ?? execFile3, opts.worktreePath, filename);
83894
+ const check = await checkIgnored(opts.execFileImpl ?? execFile4, opts.worktreePath, filename);
83454
83895
  if (!check.ignored) {
83455
83896
  await opts.audit?.filesystem({ type: "secret:env-write-skipped", target: opts.taskId, metadata: { filename, reason: "not-gitignored", overwritePolicy, checkIgnoreError: check.error } });
83456
83897
  return { outcome: "skipped", filename, reason: "not-gitignored" };
@@ -84968,11 +85409,11 @@ var init_step_session_executor = __esm({
84968
85409
  }
84969
85410
  if (attempt > 0) {
84970
85411
  retries++;
84971
- const delay4 = RETRY_DELAYS_MS[attempt - 1] ?? 15e3;
85412
+ const delay5 = RETRY_DELAYS_MS[attempt - 1] ?? 15e3;
84972
85413
  stepExecLog.log(
84973
- `Step ${stepIndex} retry ${attempt}/${MAX_STEP_RETRIES} (delay=${delay4}ms) for task ${taskDetail.id}`
85414
+ `Step ${stepIndex} retry ${attempt}/${MAX_STEP_RETRIES} (delay=${delay5}ms) for task ${taskDetail.id}`
84974
85415
  );
84975
- await sleep3(delay4);
85416
+ await sleep3(delay5);
84976
85417
  }
84977
85418
  const agentLogger = new AgentLogger({
84978
85419
  store: this.store,
@@ -85738,9 +86179,9 @@ async function withRateLimitRetry(fn2, options = {}) {
85738
86179
  }
85739
86180
  const rawDelay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
85740
86181
  const jitter = rawDelay * 0.1 * (2 * Math.random() - 1);
85741
- const delay4 = Math.max(0, Math.round(rawDelay + jitter));
85742
- onRetry?.(attempt + 1, delay4, error);
85743
- await sleep4(delay4, signal);
86182
+ const delay5 = Math.max(0, Math.round(rawDelay + jitter));
86183
+ onRetry?.(attempt + 1, delay5, error);
86184
+ await sleep4(delay5, signal);
85744
86185
  }
85745
86186
  }
85746
86187
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
@@ -88398,7 +88839,7 @@ var init_session_token_usage = __esm({
88398
88839
  });
88399
88840
 
88400
88841
  // ../engine/src/merger-squash-audit.ts
88401
- import { execFile as execFile5 } from "node:child_process";
88842
+ import { execFile as execFile6 } from "node:child_process";
88402
88843
  import { promisify as promisify24 } from "node:util";
88403
88844
  async function auditSquashMerge(input) {
88404
88845
  const normalizedLookback = normalizeMergeOverlapLookback(input.lookback);
@@ -88554,14 +88995,14 @@ var execFileAsync, MERGER_MAIN_OVERLAP_LOOKBACK_COMMITS, GIT_OUTPUT_MAX_BUFFER;
88554
88995
  var init_merger_squash_audit = __esm({
88555
88996
  "../engine/src/merger-squash-audit.ts"() {
88556
88997
  "use strict";
88557
- execFileAsync = promisify24(execFile5);
88998
+ execFileAsync = promisify24(execFile6);
88558
88999
  MERGER_MAIN_OVERLAP_LOOKBACK_COMMITS = 30;
88559
89000
  GIT_OUTPUT_MAX_BUFFER = 10 * 1024 * 1024;
88560
89001
  }
88561
89002
  });
88562
89003
 
88563
89004
  // ../engine/src/merger-overlap-guard.ts
88564
- import { execFile as execFile6 } from "node:child_process";
89005
+ import { execFile as execFile7 } from "node:child_process";
88565
89006
  import { promisify as promisify25 } from "node:util";
88566
89007
  async function getBranchTouchedFiles({
88567
89008
  rootDir,
@@ -88687,13 +89128,13 @@ var init_merger_overlap_guard = __esm({
88687
89128
  "use strict";
88688
89129
  init_logger2();
88689
89130
  init_merger_squash_audit();
88690
- execFileAsync2 = promisify25(execFile6);
89131
+ execFileAsync2 = promisify25(execFile7);
88691
89132
  GIT_OUTPUT_MAX_BUFFER2 = 10 * 1024 * 1024;
88692
89133
  }
88693
89134
  });
88694
89135
 
88695
89136
  // ../engine/src/merger-diff-volume-gate.ts
88696
- import { execFile as execFile7 } from "node:child_process";
89137
+ import { execFile as execFile8 } from "node:child_process";
88697
89138
  import { promisify as promisify26 } from "node:util";
88698
89139
  function buildMessage(findings) {
88699
89140
  const details = findings.map((finding) => `${finding.file} (branch_net=${finding.branchNet}, staged=${finding.staged}, ratio=${finding.ratio.toFixed(3)})`).join(", ");
@@ -88755,7 +89196,7 @@ var init_merger_diff_volume_gate = __esm({
88755
89196
  "../engine/src/merger-diff-volume-gate.ts"() {
88756
89197
  "use strict";
88757
89198
  init_merger();
88758
- execFileAsync3 = promisify26(execFile7);
89199
+ execFileAsync3 = promisify26(execFile8);
88759
89200
  DiffVolumeRegressionError = class extends Error {
88760
89201
  constructor(findings) {
88761
89202
  super(buildMessage(findings));
@@ -88810,10 +89251,13 @@ function decideAutoPrerebase(input) {
88810
89251
  if (hotMatches.length > 0) {
88811
89252
  return { fire: true, reason: "hot-file", commitsBehind, hotMatches };
88812
89253
  }
88813
- const threshold = input.settings.prerebaseDivergenceThreshold ?? 0;
88814
- if (threshold > 0 && commitsBehind > threshold) {
89254
+ const threshold = input.settings.prerebaseDivergenceThreshold ?? 1;
89255
+ if (threshold > 0 && commitsBehind >= threshold) {
88815
89256
  return { fire: true, reason: "divergence-threshold", commitsBehind, hotMatches: [] };
88816
89257
  }
89258
+ if (commitsBehind > 0) {
89259
+ return { fire: true, reason: "safety-fallback-any-divergence", commitsBehind, hotMatches: [] };
89260
+ }
88817
89261
  return { fire: false, reason: "no-divergence", commitsBehind, hotMatches: [] };
88818
89262
  }
88819
89263
  async function runAutoPrerebase(deps) {
@@ -89416,7 +89860,7 @@ var init_mesh_lease_manager = __esm({
89416
89860
 
89417
89861
  // ../engine/src/merger-integration-worktree.ts
89418
89862
  import { createHash as createHash10 } from "node:crypto";
89419
- import { exec as exec25, execFile as execFile8 } from "node:child_process";
89863
+ import { exec as exec25, execFile as execFile9 } from "node:child_process";
89420
89864
  import { promisify as promisify29 } from "node:util";
89421
89865
  function quoteAutostashArg(value) {
89422
89866
  return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
@@ -89895,7 +90339,7 @@ var init_merger_integration_worktree = __esm({
89895
90339
  init_worktree_pool();
89896
90340
  init_worktree_names();
89897
90341
  execAsync23 = promisify29(exec25);
89898
- execFileAsync4 = promisify29(execFile8);
90342
+ execFileAsync4 = promisify29(execFile9);
89899
90343
  MERGE_HANDOFF_WORKER_ID = "merger-reuse-handoff";
89900
90344
  MergeHandoffRefusedError = class extends Error {
89901
90345
  reason;
@@ -90608,7 +91052,7 @@ var init_worktree_acquisition = __esm({
90608
91052
  });
90609
91053
 
90610
91054
  // ../engine/src/merger-ref-update-advance.ts
90611
- import { execFile as execFile9 } from "node:child_process";
91055
+ import { execFile as execFile10 } from "node:child_process";
90612
91056
  import { promisify as promisify31 } from "node:util";
90613
91057
  async function runGit3(args, cwd) {
90614
91058
  return await execFileAsync5("git", args, {
@@ -90731,12 +91175,22 @@ async function advanceIntegrationBranchRef(args) {
90731
91175
  return { advanced: true, previousSha: expectedCurrentSha, newSha };
90732
91176
  } catch (error) {
90733
91177
  const diagnostic = error instanceof Error ? error.message : String(error);
90734
- const lower = diagnostic.toLowerCase();
90735
- const isConcurrent = lower.includes("cannot lock ref") || lower.includes("is at") || lower.includes("expected");
90736
- const reason = isConcurrent ? "concurrent-advance" : "ref-update-refused";
91178
+ let postFailureCurrentSha;
91179
+ try {
91180
+ const { stdout: postStdout } = await testHooks.runGit(
91181
+ ["rev-parse", "--verify", ref],
91182
+ rootDir
91183
+ );
91184
+ postFailureCurrentSha = postStdout.trim() || void 0;
91185
+ } catch {
91186
+ postFailureCurrentSha = observedCurrentSha;
91187
+ }
91188
+ const effectiveObserved = postFailureCurrentSha || observedCurrentSha;
91189
+ const refMoved = !!effectiveObserved && effectiveObserved !== expectedCurrentSha;
91190
+ const reason = refMoved ? "concurrent-advance" : "ref-update-refused";
90737
91191
  await emitRefAdvance({
90738
91192
  succeeded: false,
90739
- fromSha: observedCurrentSha || expectedCurrentSha,
91193
+ fromSha: effectiveObserved || expectedCurrentSha,
90740
91194
  toSha: newSha,
90741
91195
  error: `${reason}: ${diagnostic}`
90742
91196
  });
@@ -90744,7 +91198,7 @@ async function advanceIntegrationBranchRef(args) {
90744
91198
  advanced: false,
90745
91199
  reason,
90746
91200
  diagnostic,
90747
- observedCurrentSha
91201
+ observedCurrentSha: effectiveObserved
90748
91202
  };
90749
91203
  }
90750
91204
  }
@@ -90752,7 +91206,7 @@ var execFileAsync5, testHooks, IntegrationBranchConcurrentAdvanceError;
90752
91206
  var init_merger_ref_update_advance = __esm({
90753
91207
  "../engine/src/merger-ref-update-advance.ts"() {
90754
91208
  "use strict";
90755
- execFileAsync5 = promisify31(execFile9);
91209
+ execFileAsync5 = promisify31(execFile10);
90756
91210
  testHooks = {
90757
91211
  runGit: runGit3
90758
91212
  };
@@ -90779,7 +91233,7 @@ var init_merger_ref_update_advance = __esm({
90779
91233
  });
90780
91234
 
90781
91235
  // ../engine/src/worktree-ref-sync.ts
90782
- import { execFile as execFile10, spawn as spawn5 } from "node:child_process";
91236
+ import { execFile as execFile11, spawn as spawn5 } from "node:child_process";
90783
91237
  import { promisify as promisify32 } from "node:util";
90784
91238
  import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
90785
91239
  import { dirname as dirname17, join as join40 } from "node:path";
@@ -91023,7 +91477,7 @@ var execFileAsync6;
91023
91477
  var init_worktree_ref_sync = __esm({
91024
91478
  "../engine/src/worktree-ref-sync.ts"() {
91025
91479
  "use strict";
91026
- execFileAsync6 = promisify32(execFile10);
91480
+ execFileAsync6 = promisify32(execFile11);
91027
91481
  }
91028
91482
  });
91029
91483
 
@@ -91234,6 +91688,7 @@ __export(merger_exports, {
91234
91688
  classifyConflict: () => classifyConflict,
91235
91689
  classifyOwnedLandedEvidence: () => classifyOwnedLandedEvidence,
91236
91690
  commitOrAmendMergeWithFixes: () => commitOrAmendMergeWithFixes,
91691
+ composeMergeCommitBody: () => composeMergeCommitBody,
91237
91692
  deriveDeterministicSubjectSummary: () => deriveDeterministicSubjectSummary,
91238
91693
  deriveScopedPnpmTestCommand: () => deriveScopedPnpmTestCommand,
91239
91694
  detectResolvableConflicts: () => detectResolvableConflicts,
@@ -91294,7 +91749,7 @@ __export(merger_exports, {
91294
91749
  tryFastForwardFromOrigin: () => tryFastForwardFromOrigin,
91295
91750
  validateDiffScope: () => validateDiffScope
91296
91751
  });
91297
- import { execSync as execSync4, exec as exec28, execFile as execFile11 } from "node:child_process";
91752
+ import { execSync as execSync4, exec as exec28, execFile as execFile12 } from "node:child_process";
91298
91753
  import { promisify as promisify34 } from "node:util";
91299
91754
  import { existsSync as existsSync37, readFileSync as readFileSync12, readdirSync as readdirSync5, writeFileSync as writeFileSync3, unlinkSync, renameSync as renameSync2 } from "node:fs";
91300
91755
  import { createHash as createHash11 } from "node:crypto";
@@ -93723,6 +94178,23 @@ async function generateAiMergeSummary(commitLog, diffStat, settings, rootDir) {
93723
94178
  return null;
93724
94179
  }
93725
94180
  }
94181
+ async function generateAiMergeBody(commitLog, diffStat, settings, rootDir, branch, taskId, signal) {
94182
+ const cleanStat = diffStat.trim();
94183
+ if (!cleanStat) return null;
94184
+ try {
94185
+ const resolved = resolveTitleSummarizerSettingsModel(settings);
94186
+ return await summarizeCommitBody(cleanStat, rootDir, resolved.provider, resolved.modelId, {
94187
+ branch,
94188
+ taskId,
94189
+ commitLog,
94190
+ signal
94191
+ });
94192
+ } catch (err) {
94193
+ const message = err instanceof Error ? err.message : String(err);
94194
+ mergerLog.warn(`AI merge body failed; using deterministic fallback (${message})`);
94195
+ return null;
94196
+ }
94197
+ }
93726
94198
  async function generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, signal) {
93727
94199
  try {
93728
94200
  const resolved = resolveTitleSummarizerSettingsModel(settings);
@@ -93790,22 +94262,43 @@ function deriveDeterministicSubjectSummary(commitLog) {
93790
94262
  const summary = extras > 0 ? `${headline} (+${extras} more)` : headline;
93791
94263
  return summary;
93792
94264
  }
93793
- async function buildDeterministicMergeMessage(params) {
93794
- const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary, aiSubject } = params;
93795
- const prefix = includeTaskId ? `feat(${taskId})` : "feat";
93796
- const trimmedAiSubject = aiSubject?.trim() ?? "";
93797
- const derived = trimmedAiSubject.length === 0 ? deriveDeterministicSubjectSummary(commitLog ?? "") : null;
93798
- const subjectSummary = trimmedAiSubject.length > 0 ? trimmedAiSubject : derived ?? `merge ${branch}`;
93799
- const subject = `${prefix}: ${subjectSummary}`;
94265
+ function composeMergeCommitBody(params) {
94266
+ const { branch, commitLog, diffStat, aiSummary, aiBody } = params;
94267
+ const trimmedSummary = aiSummary?.trim() ?? "";
94268
+ const trimmedAiBody = aiBody?.trim() ?? "";
93800
94269
  const trimmedCommitLog = commitLog?.trim() ?? "";
93801
94270
  const trimmedDiffStat = diffStat?.trim() ?? "";
93802
94271
  const commitsSection = trimmedCommitLog.length > 0 ? trimmedCommitLog : `- merge ${branch}`;
93803
- const body = aiSummary?.trim().length ? aiSummary.trim() : [
94272
+ const parts = [];
94273
+ if (trimmedSummary.length > 0) parts.push(trimmedSummary);
94274
+ if (trimmedAiBody.length > 0) parts.push(trimmedAiBody);
94275
+ const deterministicFallback = [
93804
94276
  `Commits merged:
93805
94277
  ${commitsSection}`,
93806
94278
  trimmedDiffStat.length > 0 ? `Files changed:
93807
94279
  ${trimmedDiffStat}` : ""
93808
94280
  ].filter(Boolean).join("\n\n");
94281
+ if (parts.length === 0) return deterministicFallback;
94282
+ if (trimmedDiffStat.length > 0) {
94283
+ parts.push(`Files changed:
94284
+ ${trimmedDiffStat}`);
94285
+ }
94286
+ return parts.join("\n\n");
94287
+ }
94288
+ async function buildDeterministicMergeMessage(params) {
94289
+ const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary, aiBody, aiSubject } = params;
94290
+ const prefix = includeTaskId ? `feat(${taskId})` : "feat";
94291
+ const trimmedAiSubject = aiSubject?.trim() ?? "";
94292
+ const derived = trimmedAiSubject.length === 0 ? deriveDeterministicSubjectSummary(commitLog ?? "") : null;
94293
+ const subjectSummary = trimmedAiSubject.length > 0 ? trimmedAiSubject : derived ?? `merge ${branch}`;
94294
+ const subject = `${prefix}: ${subjectSummary}`;
94295
+ const body = composeMergeCommitBody({
94296
+ branch,
94297
+ commitLog,
94298
+ diffStat,
94299
+ aiSummary,
94300
+ aiBody
94301
+ });
93809
94302
  const escape = (s3) => s3.replace(/(["\\$`])/g, "\\$1");
93810
94303
  return {
93811
94304
  subjectArg: `-m "${escape(subject)}"`,
@@ -94217,6 +94710,7 @@ ${msg}`;
94217
94710
  diffStat: messageDiffStat,
94218
94711
  includeTaskId,
94219
94712
  aiSummary,
94713
+ aiBody: void 0,
94220
94714
  aiSubject
94221
94715
  });
94222
94716
  let lineageId;
@@ -97410,8 +97904,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97410
97904
  mergerLog.log(`${taskId}: ${routeMessage}`);
97411
97905
  await store.appendAgentLog(taskId, routeMessage, "text", void 0, "merger");
97412
97906
  }
97413
- const aiMergeSummary = settings.useAiMergeCommitSummary ? await generateAiMergeSummary(commitLog, diffStat, settings, rootDir) : null;
97414
- const aiMergeSubject = settings.useAiMergeCommitSummary ? await generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal) : null;
97907
+ const [aiMergeSummary, aiMergeBody, aiMergeSubject] = settings.useAiMergeCommitSummary ? await Promise.all([
97908
+ generateAiMergeSummary(commitLog, diffStat, settings, rootDir),
97909
+ generateAiMergeBody(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal),
97910
+ generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal)
97911
+ ]) : [null, null, null];
97415
97912
  try {
97416
97913
  const scopeResult = await validateDiffScope(store, taskId, diffStat, settings.strictScopeEnforcement);
97417
97914
  for (const warning of scopeResult.warnings) {
@@ -97487,6 +97984,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97487
97984
  commitLog,
97488
97985
  diffStat,
97489
97986
  aiSummary: aiMergeSummary,
97987
+ aiBody: aiMergeBody,
97490
97988
  aiSubject: aiMergeSubject,
97491
97989
  includeTaskId,
97492
97990
  sourceIssueRef,
@@ -98085,6 +98583,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98085
98583
  const recordedFilesChanged = mergeWasEmpty ? 0 : filesChanged;
98086
98584
  const recordedInsertions = mergeWasEmpty ? 0 : insertions;
98087
98585
  const recordedDeletions = mergeWasEmpty ? 0 : deletions;
98586
+ const mergeConfirmedAtThisPoint = !reuseTaskWorktreeMerge;
98088
98587
  const mergeDetails = {
98089
98588
  commitSha: recordedSha,
98090
98589
  rebaseBaseSha: !mergeWasEmpty && rebaseMergeBaseSha ? rebaseMergeBaseSha : void 0,
@@ -98095,9 +98594,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98095
98594
  noOpVerifiedShortCircuit,
98096
98595
  landedFilesAttributionRestricted,
98097
98596
  landedFilesCaptureFallback,
98597
+ // Keep mergeDetails headline-only for dashboard cards; rich bullet body
98598
+ // is used in the actual git commit message composition path.
98098
98599
  mergeCommitMessage: aiMergeSummary || commitLog,
98099
98600
  mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
98100
- mergeConfirmed: true,
98601
+ mergeConfirmed: mergeConfirmedAtThisPoint,
98101
98602
  mergeTargetBranch: mergeTarget.branch,
98102
98603
  mergeTargetSource: mergeTarget.source,
98103
98604
  resolutionStrategy: result.resolutionStrategy,
@@ -98191,6 +98692,19 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98191
98692
  mergerLog.log(
98192
98693
  `${taskId}: ${integrationBranch2} advanced to ${worktreeHeadSha.slice(0, 8)} via update-ref; your checked-out worktree at ${projectRootDir} is now behind`
98193
98694
  );
98695
+ try {
98696
+ const currentTask = await store.getTask(taskId).catch(() => null);
98697
+ const currentMergeDetails = currentTask?.mergeDetails;
98698
+ if (currentMergeDetails && !currentMergeDetails.mergeConfirmed) {
98699
+ await store.updateTask(taskId, {
98700
+ mergeDetails: { ...currentMergeDetails, mergeConfirmed: true }
98701
+ });
98702
+ }
98703
+ } catch (promoteErr) {
98704
+ mergerLog.warn(
98705
+ `${taskId}: failed to promote mergeConfirmed post-ref-advance: ${promoteErr instanceof Error ? promoteErr.message : String(promoteErr)}`
98706
+ );
98707
+ }
98194
98708
  const autoSyncMode = normalizeMergeAdvanceAutoSyncMode(settings.mergeAdvanceAutoSync);
98195
98709
  if (autoSyncMode !== "off") {
98196
98710
  try {
@@ -98555,6 +99069,7 @@ async function executeMergeAttempt(params, aiTracker) {
98555
99069
  commitLog,
98556
99070
  diffStat,
98557
99071
  aiSummary,
99072
+ aiBody,
98558
99073
  aiSubject,
98559
99074
  includeTaskId,
98560
99075
  sourceIssueRef,
@@ -98813,6 +99328,7 @@ async function executeMergeAttempt(params, aiTracker) {
98813
99328
  commitLog,
98814
99329
  diffStat,
98815
99330
  aiSummary,
99331
+ aiBody,
98816
99332
  aiSubject,
98817
99333
  includeTaskId,
98818
99334
  hasConflicts,
@@ -98959,7 +99475,7 @@ async function attemptWithMixedSideStrategy(params, strategy, aiTracker) {
98959
99475
  }
98960
99476
  }
98961
99477
  async function finalizeSideStrategyAttempt(params, side, aiTracker) {
98962
- const { rootDir, branch, commitLog, diffStat, aiSummary, aiSubject, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
99478
+ const { rootDir, branch, commitLog, diffStat, aiSummary, aiBody, aiSubject, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
98963
99479
  const staged = execSyncText("git diff --cached --quiet 2>&1; echo $?", {
98964
99480
  cwd: rootDir,
98965
99481
  encoding: "utf-8"
@@ -99000,7 +99516,8 @@ async function finalizeSideStrategyAttempt(params, side, aiTracker) {
99000
99516
  commitLog,
99001
99517
  diffStat,
99002
99518
  includeTaskId,
99003
- aiSummary: aiSummary?.trim().length ? aiSummary : safeBody,
99519
+ aiSummary,
99520
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
99004
99521
  aiSubject
99005
99522
  });
99006
99523
  await enforceSquashFileScopeInvariant({
@@ -99058,6 +99575,7 @@ async function runAiAgentForCommit(params) {
99058
99575
  commitLog,
99059
99576
  diffStat,
99060
99577
  aiSummary,
99578
+ aiBody,
99061
99579
  aiSubject,
99062
99580
  includeTaskId,
99063
99581
  hasConflicts,
@@ -99279,7 +99797,8 @@ async function runAiAgentForCommit(params) {
99279
99797
  commitLog,
99280
99798
  diffStat,
99281
99799
  includeTaskId,
99282
- aiSummary: aiSummary?.trim().length ? aiSummary : safeBody,
99800
+ aiSummary,
99801
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
99283
99802
  aiSubject
99284
99803
  });
99285
99804
  await runDiffVolumeGate({
@@ -99734,7 +100253,7 @@ var init_merger = __esm({
99734
100253
  init_merger_diff_volume_gate();
99735
100254
  init_merger_ref_update_advance();
99736
100255
  execAsync26 = promisify34(exec28);
99737
- execFileAsync7 = promisify34(execFile11);
100256
+ execFileAsync7 = promisify34(execFile12);
99738
100257
  LOCKFILE_PATTERNS = [
99739
100258
  "package-lock.json",
99740
100259
  "pnpm-lock.yaml",
@@ -100104,7 +100623,7 @@ function isNoTaskDoneFailure2(task) {
100104
100623
  function hasStepProgress2(task) {
100105
100624
  return task.steps.some((step) => step.status !== "pending");
100106
100625
  }
100107
- 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;
100626
+ 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;
100108
100627
  var init_self_healing = __esm({
100109
100628
  "../engine/src/self-healing.ts"() {
100110
100629
  "use strict";
@@ -100125,6 +100644,8 @@ var init_self_healing = __esm({
100125
100644
  init_foreign_only_contamination();
100126
100645
  init_notifier();
100127
100646
  init_dependency_blocked_todo_reporter();
100647
+ init_transient_merge_error_classifier();
100648
+ init_transient_merge_error_classifier();
100128
100649
  log20 = createLogger2("self-healing");
100129
100650
  worktreeMetadataReconcileLog = createLogger2("worktree-metadata-reconcile");
100130
100651
  execAsync27 = promisify35(exec29);
@@ -100163,6 +100684,7 @@ var init_self_healing = __esm({
100163
100684
  MAX_TASK_DONE_RETRIES = 3;
100164
100685
  MAX_WORKTREE_SESSION_RETRIES = 3;
100165
100686
  MAX_AUTO_MERGE_RETRIES = 3;
100687
+ MAX_TRANSIENT_MERGE_RECOVERIES = 2;
100166
100688
  MAX_STARVATION_DROPS = 3;
100167
100689
  DEADLOCK_RECOVERY_COOLDOWN_MS = 15 * 6e4;
100168
100690
  DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS = 5 * 6e4;
@@ -100360,6 +100882,7 @@ var init_self_healing = __esm({
100360
100882
  { name: "stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks().then(() => void 0) },
100361
100883
  { name: "failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps().then(() => void 0) },
100362
100884
  { name: "interrupted-merging", fn: () => this.recoverInterruptedMergingTasks().then(() => void 0) },
100885
+ { name: "transient-merge-failures", fn: () => this.recoverTransientMergeFailures().then(() => void 0) },
100363
100886
  { name: "done-merge-metadata", fn: () => this.recoverDoneTaskMergeMetadata().then(() => void 0) },
100364
100887
  { name: "reconcile-done-task-integrity", fn: () => this.reconcileDoneTaskIntegrity().then(() => void 0) },
100365
100888
  // FN-5092: must run BEFORE any merger pickup path so the merger queue is
@@ -100454,8 +100977,8 @@ var init_self_healing = __esm({
100454
100977
  this.lastPauseTriggeredAt = Date.now();
100455
100978
  const baseDelay = settings.autoUnpauseBaseDelayMs ?? 3e5;
100456
100979
  const maxDelay = settings.autoUnpauseMaxDelayMs ?? 36e5;
100457
- const delay4 = Math.min(baseDelay * Math.pow(2, this.unpauseAttempt), maxDelay);
100458
- this.scheduleUnpause(delay4);
100980
+ const delay5 = Math.min(baseDelay * Math.pow(2, this.unpauseAttempt), maxDelay);
100981
+ this.scheduleUnpause(delay5);
100459
100982
  }
100460
100983
  if (previous.globalPause && !settings.globalPause) {
100461
100984
  this.cancelUnpauseTimer();
@@ -100889,6 +101412,7 @@ var init_self_healing = __esm({
100889
101412
  { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
100890
101413
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
100891
101414
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
101415
+ { name: "recover-transient-merge-failures", fn: () => this.recoverTransientMergeFailures() },
100892
101416
  { name: "recover-done-merge-metadata", fn: () => this.recoverDoneTaskMergeMetadata() },
100893
101417
  { name: "recover-stale-merging-status", fn: () => this.recoverStaleMergingStatus() },
100894
101418
  { name: "finalize-noop-review", fn: () => this.finalizeNoOpReviewTasks() },
@@ -104061,6 +104585,142 @@ var init_self_healing = __esm({
104061
104585
  * No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
104062
104586
  * @returns Number of tasks finalized or unblocked
104063
104587
  */
104588
+ /**
104589
+ * FN-5627 follow-up: recover in-review tasks that are stuck with
104590
+ * `mergeRetries >= MAX_AUTO_MERGE_RETRIES` and `status='failed'` due to a
104591
+ * TRANSIENT merge failure class (e.g., `target-not-queued` lease handoff
104592
+ * race, legacy same-SHA spurious concurrent-advance). These tasks are
104593
+ * NOT really stuck — they just hit a race or a misclassified ref-update
104594
+ * failure that would have cleared on a fresh attempt. Without this sweep,
104595
+ * the only path forward is manual intervention (the
104596
+ * `AUTO_MERGE_COOLDOWN_MS`-based reset takes hours).
104597
+ *
104598
+ * For each matching task:
104599
+ * - Reset `mergeRetries=0`, clear `status` and `error`.
104600
+ * - Increment `mergeDetails.transientRecoveryCount`.
104601
+ * - Re-enqueue via `requeueForAutoMerge`.
104602
+ * - Bounded by `MAX_TRANSIENT_MERGE_RECOVERIES`; exhausted tasks stay
104603
+ * parked as failed and emit `merger:transient-failure-budget-exhausted`
104604
+ * once for diagnostic visibility.
104605
+ *
104606
+ * No-op when `settings.autoMerge === false`, no `requeueForAutoMerge`
104607
+ * callback is wired, or global/engine pause is active.
104608
+ *
104609
+ * @returns Number of tasks recovered
104610
+ */
104611
+ async recoverTransientMergeFailures() {
104612
+ const requeue = this.options.requeueForAutoMerge;
104613
+ if (!requeue) return 0;
104614
+ try {
104615
+ const settings = await this.store.getSettings();
104616
+ if (settings.autoMerge === false) return 0;
104617
+ if (settings.globalPause || settings.enginePaused) return 0;
104618
+ const slim = await this.store.listTasks({ column: "in-review", slim: true });
104619
+ const candidates = slim.filter(
104620
+ (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
104621
+ );
104622
+ if (candidates.length === 0) return 0;
104623
+ log20.warn(
104624
+ `Found ${candidates.length} in-review task(s) with transient merge failures stuck at mergeRetries=${MAX_AUTO_MERGE_RETRIES}; attempting auto-recovery`
104625
+ );
104626
+ let recovered = 0;
104627
+ for (const slimTask of candidates) {
104628
+ const task = await this.store.getTask(slimTask.id).catch(() => null);
104629
+ if (!task) continue;
104630
+ if (task.column !== "in-review" || task.status !== "failed" || (task.mergeRetries ?? 0) < MAX_AUTO_MERGE_RETRIES) {
104631
+ continue;
104632
+ }
104633
+ const errorText = task.error ?? "";
104634
+ const transientClass = classifyTransientMergeError(errorText);
104635
+ if (!transientClass) continue;
104636
+ const currentCount = task.mergeDetails?.transientRecoveryCount ?? 0;
104637
+ const errorSnippet = errorText.slice(0, 200);
104638
+ const audit = createRunAuditor(this.store, {
104639
+ runId: generateSyntheticRunId("self-heal-transient-merge-recovery", task.id),
104640
+ agentId: "self-healing",
104641
+ taskId: task.id,
104642
+ phase: "recover-transient-merge-failures"
104643
+ });
104644
+ if (currentCount >= MAX_TRANSIENT_MERGE_RECOVERIES) {
104645
+ if (!errorText.includes("[transient-recovery-budget-exhausted]")) {
104646
+ await this.store.logEntry(
104647
+ task.id,
104648
+ `[FN-5627] Transient merge failure auto-recovery budget exhausted (${transientClass}, ${currentCount}/${MAX_TRANSIENT_MERGE_RECOVERIES}). Task remains parked in in-review for manual review.`
104649
+ );
104650
+ await this.store.updateTask(task.id, {
104651
+ error: `${errorText} [transient-recovery-budget-exhausted]`
104652
+ });
104653
+ try {
104654
+ await audit.database({
104655
+ type: "merger:transient-failure-budget-exhausted",
104656
+ target: task.id,
104657
+ metadata: {
104658
+ taskId: task.id,
104659
+ transientClass,
104660
+ recoveryCount: currentCount,
104661
+ maxRecoveries: MAX_TRANSIENT_MERGE_RECOVERIES,
104662
+ errorSnippet
104663
+ }
104664
+ });
104665
+ } catch (auditErr) {
104666
+ log20.warn(
104667
+ `recoverTransientMergeFailures: audit emit failed for ${task.id}: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
104668
+ );
104669
+ }
104670
+ }
104671
+ continue;
104672
+ }
104673
+ const nextCount = currentCount + 1;
104674
+ await this.store.logEntry(
104675
+ task.id,
104676
+ `Auto-recovered: transient merge failure (${transientClass}); resetting mergeRetries=0 and re-enqueueing (recovery ${nextCount}/${MAX_TRANSIENT_MERGE_RECOVERIES}) [FN-5627]`
104677
+ );
104678
+ await this.store.updateTask(task.id, {
104679
+ mergeRetries: 0,
104680
+ status: null,
104681
+ error: null,
104682
+ mergeDetails: {
104683
+ ...task.mergeDetails,
104684
+ transientRecoveryCount: nextCount
104685
+ }
104686
+ });
104687
+ try {
104688
+ await audit.database({
104689
+ type: "merger:transient-failure-auto-recovered",
104690
+ target: task.id,
104691
+ metadata: {
104692
+ taskId: task.id,
104693
+ transientClass,
104694
+ mergeRetries: task.mergeRetries ?? 0,
104695
+ recoveryCount: nextCount,
104696
+ errorSnippet
104697
+ }
104698
+ });
104699
+ } catch (auditErr) {
104700
+ log20.warn(
104701
+ `recoverTransientMergeFailures: audit emit failed for ${task.id}: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
104702
+ );
104703
+ }
104704
+ try {
104705
+ await requeue(task.id);
104706
+ recovered++;
104707
+ } catch (requeueErr) {
104708
+ log20.warn(
104709
+ `recoverTransientMergeFailures: requeue failed for ${task.id}: ${requeueErr instanceof Error ? requeueErr.message : String(requeueErr)}`
104710
+ );
104711
+ }
104712
+ }
104713
+ if (recovered > 0) {
104714
+ log20.log(`recoverTransientMergeFailures: re-enqueued ${recovered} stuck in-review task(s)`);
104715
+ }
104716
+ return recovered;
104717
+ } catch (err) {
104718
+ log20.warn(
104719
+ `recoverTransientMergeFailures sweep failed: ${err instanceof Error ? err.message : String(err)}`
104720
+ );
104721
+ return 0;
104722
+ }
104723
+ }
104064
104724
  async recoverInterruptedMergingTasks() {
104065
104725
  try {
104066
104726
  const settings = await this.store.getSettings();
@@ -106362,7 +107022,7 @@ var init_self_healing = __esm({
106362
107022
  });
106363
107023
 
106364
107024
  // ../engine/src/triage.ts
106365
- import { Type as Type3 } from "@mariozechner/pi-ai";
107025
+ import { Type as Type3 } from "@earendil-works/pi-ai";
106366
107026
  import { exec as exec30 } from "node:child_process";
106367
107027
  import { readFile as readFile21 } from "node:fs/promises";
106368
107028
  import { join as join44 } from "node:path";
@@ -106421,9 +107081,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
106421
107081
  return { attachmentContents, imageContents };
106422
107082
  }
106423
107083
  const { readFile: readFile29 } = await import("node:fs/promises");
106424
- const { join: join58 } = await import("node:path");
107084
+ const { join: join59 } = await import("node:path");
106425
107085
  for (const att of attachments) {
106426
- const filePath = join58(
107086
+ const filePath = join59(
106427
107087
  rootDir,
106428
107088
  ".fusion",
106429
107089
  "tasks",
@@ -107896,8 +108556,8 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
107896
108556
  });
107897
108557
  if (decision.shouldRetry) {
107898
108558
  const attempt = decision.nextState.recoveryRetryCount;
107899
- const delay4 = formatDelay(decision.delayMs);
107900
- const retryMessage = `Spec review not approved (${verdictDesc}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}.`;
108559
+ const delay5 = formatDelay(decision.delayMs);
108560
+ const retryMessage = `Spec review not approved (${verdictDesc}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}.`;
107901
108561
  planLog.warn(`${task.id} ${retryMessage}`);
107902
108562
  await this.store.logEntry(task.id, retryMessage);
107903
108563
  const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
@@ -108003,10 +108663,10 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
108003
108663
  });
108004
108664
  if (decision.shouldRetry) {
108005
108665
  const attempt = decision.nextState.recoveryRetryCount;
108006
- const delay4 = formatDelay(decision.delayMs);
108666
+ const delay5 = formatDelay(decision.delayMs);
108007
108667
  if (!isSilentTransientError(errorMessage)) {
108008
- planLog.warn(`\u26A1 ${task.id} transient error during triage \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
108009
- await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`).catch((err2) => {
108668
+ planLog.warn(`\u26A1 ${task.id} transient error during triage \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}: ${errorMessage}`);
108669
+ await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`).catch((err2) => {
108010
108670
  const msg = err2 instanceof Error ? err2.message : String(err2);
108011
108671
  planLog.warn(`${task.id}: failed to log transient-error retry entry: ${msg}`);
108012
108672
  });
@@ -108305,9 +108965,9 @@ Remove or replace these ids and call fn_task_create again.`
108305
108965
  }
108306
108966
  try {
108307
108967
  const { readFile: readFile29 } = await import("node:fs/promises");
108308
- const { join: join58 } = await import("node:path");
108968
+ const { join: join59 } = await import("node:path");
108309
108969
  const promptContent = await readFile29(
108310
- join58(rootDir, promptPath),
108970
+ join59(rootDir, promptPath),
108311
108971
  "utf-8"
108312
108972
  ).catch((err) => {
108313
108973
  const msg = err instanceof Error ? err.message : String(err);
@@ -108782,7 +109442,7 @@ Take a completely different approach to writing this specification. Do NOT repea
108782
109442
  });
108783
109443
 
108784
109444
  // ../engine/src/merger-orphan-rehome.ts
108785
- import { execFile as execFile12 } from "node:child_process";
109445
+ import { execFile as execFile13 } from "node:child_process";
108786
109446
  import { promisify as promisify37 } from "node:util";
108787
109447
  async function runGit5(args, cwd) {
108788
109448
  const { stdout } = await execFileAsync8("git", args, {
@@ -108888,7 +109548,7 @@ var init_merger_orphan_rehome = __esm({
108888
109548
  "../engine/src/merger-orphan-rehome.ts"() {
108889
109549
  "use strict";
108890
109550
  init_merger_ref_update_advance();
108891
- execFileAsync8 = promisify37(execFile12);
109551
+ execFileAsync8 = promisify37(execFile13);
108892
109552
  }
108893
109553
  });
108894
109554
 
@@ -109453,7 +110113,7 @@ var init_task_completion = __esm({
109453
110113
  import { spawn as spawn6 } from "node:child_process";
109454
110114
  import { existsSync as existsSync40 } from "node:fs";
109455
110115
  import { isAbsolute as isAbsolute13, join as join46 } from "node:path";
109456
- import { Type as Type4 } from "@mariozechner/pi-ai";
110116
+ import { Type as Type4 } from "@earendil-works/pi-ai";
109457
110117
  function createBuffer() {
109458
110118
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
109459
110119
  }
@@ -109762,8 +110422,8 @@ import { setImmediate as setImmediateCb3 } from "node:timers";
109762
110422
  import { delimiter, isAbsolute as isAbsolute14, join as join47, relative as relative11, resolve as resolvePath } from "node:path";
109763
110423
  import { existsSync as existsSync41, realpathSync as realpathSync6 } from "node:fs";
109764
110424
  import { readFile as readFile22, rm as rm5, writeFile as writeFile16 } from "node:fs/promises";
109765
- import { Type as Type5 } from "@mariozechner/pi-ai";
109766
- import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
110425
+ import { Type as Type5 } from "@earendil-works/pi-ai";
110426
+ import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
109767
110427
  function getResumeOrphanDelayMs() {
109768
110428
  const raw = process.env.FUSION_RESUME_ORPHAN_DELAY_MS;
109769
110429
  if (raw !== void 0) {
@@ -109780,6 +110440,26 @@ function canonicalizePath2(path6) {
109780
110440
  return resolvePath(path6);
109781
110441
  }
109782
110442
  }
110443
+ function isTransientMissingTaskJsonError(error, task) {
110444
+ if (error instanceof TaskDeletedError) {
110445
+ return false;
110446
+ }
110447
+ const message = typeof error === "string" ? error : error instanceof Error ? error.message : "";
110448
+ const match = TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN.exec(message);
110449
+ if (!match) {
110450
+ return false;
110451
+ }
110452
+ const [, filePath, taskIdFromPath] = match;
110453
+ if (taskIdFromPath !== task.id) {
110454
+ return false;
110455
+ }
110456
+ if (typeof task.worktree !== "string" || task.worktree.length === 0) {
110457
+ return false;
110458
+ }
110459
+ const normalizedWorktree = resolvePath(task.worktree);
110460
+ const normalizedTaskJsonPath = resolvePath(filePath);
110461
+ return normalizedTaskJsonPath.startsWith(`${normalizedWorktree}/`);
110462
+ }
109783
110463
  function detectPendingReviewBlock(task, codeReviewVerdicts) {
109784
110464
  const inProgressStepIndices = [];
109785
110465
  for (let stepIndex = 0; stepIndex < task.steps.length; stepIndex++) {
@@ -110315,7 +110995,7 @@ function detectReviewHandoffIntent(commentText) {
110315
110995
  ];
110316
110996
  return handoffPhrases.some((phrase) => text.includes(phrase));
110317
110997
  }
110318
- 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;
110998
+ 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;
110319
110999
  var init_executor = __esm({
110320
111000
  "../engine/src/executor.ts"() {
110321
111001
  "use strict";
@@ -110383,6 +111063,7 @@ var init_executor = __esm({
110383
111063
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
110384
111064
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
110385
111065
  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.";
111066
+ TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN = /ENOENT:\s+no such file or directory,\s+open\s+'([^']+\/\.fusion\/tasks\/([^/]+)\/task\.json)'/;
110386
111067
  DISSENT_PATTERNS = [
110387
111068
  /\btask (is|was)(?: not|n['’]?t) complete\b/i,
110388
111069
  /\b(?:i (?:could|can)(?:not|n['’]?t)|unable to|failed to) (?:complete|finish|implement)\b/i,
@@ -112874,10 +113555,10 @@ ${summary}`,
112874
113555
  });
112875
113556
  if (decision.shouldRetry) {
112876
113557
  const attempt = decision.nextState.recoveryRetryCount;
112877
- const delay4 = formatDelay(decision.delayMs);
113558
+ const delay5 = formatDelay(decision.delayMs);
112878
113559
  if (!isSilentTransientError(errorMessage)) {
112879
- executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
112880
- await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
113560
+ executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}: ${errorMessage}`);
113561
+ await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
112881
113562
  }
112882
113563
  if (worktreePath && existsSync41(worktreePath)) {
112883
113564
  try {
@@ -113839,9 +114520,9 @@ ${summary}`,
113839
114520
  });
113840
114521
  if (decision.shouldRetry) {
113841
114522
  const attempt = decision.nextState.recoveryRetryCount;
113842
- const delay4 = formatDelay(decision.delayMs);
113843
- executorLog.warn(`\u26A1 ${task.id} context-overflow fresh-session requeue ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}`);
113844
- await this.store.logEntry(task.id, `Context-overflow fresh-session requeue (${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114523
+ const delay5 = formatDelay(decision.delayMs);
114524
+ executorLog.warn(`\u26A1 ${task.id} context-overflow fresh-session requeue ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}`);
114525
+ await this.store.logEntry(task.id, `Context-overflow fresh-session requeue (${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
113845
114526
  await this.store.updateTask(task.id, {
113846
114527
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
113847
114528
  nextRecoveryAt: decision.nextState.nextRecoveryAt,
@@ -114129,10 +114810,10 @@ ${summary}`,
114129
114810
  });
114130
114811
  if (decision.shouldRetry) {
114131
114812
  const attempt = decision.nextState.recoveryRetryCount;
114132
- const delay4 = formatDelay(decision.delayMs);
114813
+ const delay5 = formatDelay(decision.delayMs);
114133
114814
  if (!isSilentTransientError(errorMessage)) {
114134
- executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
114135
- await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114815
+ executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}: ${errorMessage}`);
114816
+ await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
114136
114817
  }
114137
114818
  if (worktreePath && existsSync41(worktreePath)) {
114138
114819
  try {
@@ -116834,8 +117515,8 @@ Review the work done in this worktree and evaluate it against the criteria in yo
116834
117515
  `Failed to create worktree after ${this.MAX_WORKTREE_RETRIES} attempts: ${errorMessage}`
116835
117516
  );
116836
117517
  }
116837
- const delay4 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
116838
- await new Promise((resolve32) => setTimeout(resolve32, delay4));
117518
+ const delay5 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
117519
+ await new Promise((resolve32) => setTimeout(resolve32, delay5));
116839
117520
  }
116840
117521
  }
116841
117522
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -117073,9 +117754,15 @@ Review the work done in this worktree and evaluate it against the criteria in yo
117073
117754
  }
117074
117755
  async recoverMissingWorktreeSessionStartFailure(task, worktreePath, error, audit) {
117075
117756
  const errorText = error instanceof Error ? error.message : String(error);
117076
- if (!isMissingWorktreeSessionStartFailure(errorText)) return false;
117757
+ const missingWorktreeFailure = isMissingWorktreeSessionStartFailure(errorText);
117758
+ const missingTaskJsonFailure = isTransientMissingTaskJsonError(error, task);
117759
+ if (!missingWorktreeFailure && !missingTaskJsonFailure) return false;
117077
117760
  const classification = classifyMissingWorktreeSessionStartFailure(errorText);
117078
- const staleWorktreePath = extractMissingWorktreePathFromSessionStartFailure(errorText) ?? worktreePath;
117761
+ const missingTaskJsonPath = errorText.match(TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN)?.[1] ?? null;
117762
+ const staleWorktreePath = extractMissingWorktreePathFromSessionStartFailure(errorText) ?? (missingTaskJsonPath ? resolvePath(missingTaskJsonPath, "..", "..", "..") : null) ?? worktreePath;
117763
+ if (missingTaskJsonFailure) {
117764
+ executorLog.log(`[transient-task-json-suppressed] taskId=${task.id} elapsedMs=0 reason=missing-task-json-under-worktree path=${missingTaskJsonPath ?? "unknown"}`);
117765
+ }
117079
117766
  await audit.git({
117080
117767
  type: "worktree:incomplete-detected",
117081
117768
  target: staleWorktreePath,
@@ -120231,6 +120918,12 @@ var init_scheduler = __esm({
120231
120918
  }
120232
120919
  const sliceIdBeforeUpdate = feature.sliceId;
120233
120920
  if (this.options.missionExecutionLoop) {
120921
+ if (!this.options.missionExecutionLoop.isRunning()) {
120922
+ schedulerLog.warn(
120923
+ `MissionExecutionLoop was not running during task completion for ${taskId}; starting loop before processing outcome`
120924
+ );
120925
+ this.options.missionExecutionLoop.start();
120926
+ }
120234
120927
  void this.options.missionExecutionLoop.processTaskOutcome(taskId).catch((err) => {
120235
120928
  schedulerLog.error(`Error in missionExecutionLoop.processTaskOutcome for ${taskId}:`, err);
120236
120929
  });
@@ -120693,19 +121386,19 @@ var init_mission_autopilot = __esm({
120693
121386
  autopilotLog.error(`Error advancing slice for mission ${missionId}:`, err);
120694
121387
  state.retryCount++;
120695
121388
  if (state.retryCount <= MAX_RETRY_ATTEMPTS) {
120696
- const delay4 = RETRY_BASE_DELAY_MS2 * Math.pow(3, state.retryCount - 1);
121389
+ const delay5 = RETRY_BASE_DELAY_MS2 * Math.pow(3, state.retryCount - 1);
120697
121390
  this.logMissionEventSafe(
120698
121391
  missionId,
120699
121392
  "autopilot_retry",
120700
121393
  `Retrying slice activation after error (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS})`,
120701
- { retryCount: state.retryCount, maxRetries: MAX_RETRY_ATTEMPTS, delayMs: delay4 }
121394
+ { retryCount: state.retryCount, maxRetries: MAX_RETRY_ATTEMPTS, delayMs: delay5 }
120702
121395
  );
120703
- autopilotLog.log(`Retrying slice activation for mission ${missionId} (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS}, delay ${delay4}ms)`);
121396
+ autopilotLog.log(`Retrying slice activation for mission ${missionId} (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS}, delay ${delay5}ms)`);
120704
121397
  setTimeout(() => {
120705
121398
  if (this.isWatching(missionId)) {
120706
121399
  void this.advanceToNextSlice(missionId);
120707
121400
  }
120708
- }, delay4);
121401
+ }, delay5);
120709
121402
  } else {
120710
121403
  this.logMissionEventSafe(
120711
121404
  missionId,
@@ -121152,7 +121845,7 @@ var init_mission_autopilot = __esm({
121152
121845
  });
121153
121846
 
121154
121847
  // ../engine/src/mission-execution-loop.ts
121155
- import { EventEmitter as EventEmitter18 } from "node:events";
121848
+ import { EventEmitter as EventEmitter19 } from "node:events";
121156
121849
  var loopLog, VALIDATION_TIMEOUT_MS, MissionExecutionLoop;
121157
121850
  var init_mission_execution_loop = __esm({
121158
121851
  "../engine/src/mission-execution-loop.ts"() {
@@ -121164,7 +121857,7 @@ var init_mission_execution_loop = __esm({
121164
121857
  init_run_audit();
121165
121858
  loopLog = createLogger2("mission-loop");
121166
121859
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
121167
- MissionExecutionLoop = class extends EventEmitter18 {
121860
+ MissionExecutionLoop = class extends EventEmitter19 {
121168
121861
  running = false;
121169
121862
  taskStore;
121170
121863
  missionStore;
@@ -121304,6 +121997,10 @@ var init_mission_execution_loop = __esm({
121304
121997
  loopLog.log(`Feature ${feature.id} loopState is "${feature.loopState}"; skipping validation`);
121305
121998
  return;
121306
121999
  }
122000
+ if (this.activeValidations.has(feature.id)) {
122001
+ loopLog.log(`Feature ${feature.id} already has an active validation; skipping duplicate trigger`);
122002
+ return;
122003
+ }
121307
122004
  const assertions = this.missionStore.listAssertionsForFeature(feature.id);
121308
122005
  if (assertions.length === 0) {
121309
122006
  loopLog.log(`Feature ${feature.id} has no linked assertions; marking as passed`);
@@ -121692,6 +122389,10 @@ ${taskContext}` : ""}`;
121692
122389
  if (runId) {
121693
122390
  this.missionStore.completeValidatorRun(runId, "passed", summary);
121694
122391
  }
122392
+ const feature = this.missionStore.getFeature(featureId);
122393
+ if (feature && feature.status !== "done") {
122394
+ this.missionStore.updateFeatureStatus(featureId, "done");
122395
+ }
121695
122396
  loopLog.log(`Feature ${featureId} passed validation`);
121696
122397
  if (this.missionAutopilot?.notifyValidationComplete) {
121697
122398
  await this.missionAutopilot.notifyValidationComplete(featureId, "passed");
@@ -121795,7 +122496,7 @@ ${taskContext}` : ""}`;
121795
122496
  });
121796
122497
 
121797
122498
  // ../engine/src/smart-pull.ts
121798
- import { execFile as execFile13 } from "node:child_process";
122499
+ import { execFile as execFile14 } from "node:child_process";
121799
122500
  import { promisify as promisify41 } from "node:util";
121800
122501
  async function runGit6(args, cwd, timeoutMs) {
121801
122502
  const result = await execFileAsync9("git", args, {
@@ -121953,7 +122654,7 @@ var execFileAsync9;
121953
122654
  var init_smart_pull = __esm({
121954
122655
  "../engine/src/smart-pull.ts"() {
121955
122656
  "use strict";
121956
- execFileAsync9 = promisify41(execFile13);
122657
+ execFileAsync9 = promisify41(execFile14);
121957
122658
  }
121958
122659
  });
121959
122660
 
@@ -122902,7 +123603,7 @@ var init_room_coordination = __esm({
122902
123603
  });
122903
123604
 
122904
123605
  // ../engine/src/agent-heartbeat.ts
122905
- import { Type as Type6 } from "@mariozechner/pi-ai";
123606
+ import { Type as Type6 } from "@earendil-works/pi-ai";
122906
123607
  import { createHash as createHash12 } from "node:crypto";
122907
123608
  function adjustHeartbeatMemoryPrimer(basePrompt, mode) {
122908
123609
  if (mode === "full") return basePrompt;
@@ -130033,7 +130734,7 @@ var init_stuck_task_detector = __esm({
130033
130734
  });
130034
130735
 
130035
130736
  // ../engine/src/plugin-runner.ts
130036
- import { Type as Type7 } from "@mariozechner/pi-ai";
130737
+ import { Type as Type7 } from "@earendil-works/pi-ai";
130037
130738
  import { isAbsolute as isAbsolute16 } from "node:path";
130038
130739
  var DEFAULT_HOOK_TIMEOUT_MS, PluginRunner;
130039
130740
  var init_plugin_runner = __esm({
@@ -131184,7 +131885,7 @@ var init_task_agent_sync = __esm({
131184
131885
  });
131185
131886
 
131186
131887
  // ../engine/src/runtimes/in-process-runtime.ts
131187
- import { EventEmitter as EventEmitter19 } from "node:events";
131888
+ import { EventEmitter as EventEmitter20 } from "node:events";
131188
131889
  import { setImmediate as setImmediateCb4 } from "node:timers";
131189
131890
  var yieldEventLoop4, InProcessRuntime;
131190
131891
  var init_in_process_runtime = __esm({
@@ -131214,7 +131915,7 @@ var init_in_process_runtime = __esm({
131214
131915
  init_task_agent_sync();
131215
131916
  init_run_audit();
131216
131917
  yieldEventLoop4 = () => new Promise((resolve32) => setImmediateCb4(resolve32));
131217
- InProcessRuntime = class extends EventEmitter19 {
131918
+ InProcessRuntime = class extends EventEmitter20 {
131218
131919
  /**
131219
131920
  * @param config - Runtime configuration
131220
131921
  * @param centralCore - CentralCore reference for global coordination
@@ -132244,14 +132945,14 @@ var init_ipc_protocol = __esm({
132244
132945
  });
132245
132946
 
132246
132947
  // ../engine/src/ipc/ipc-host.ts
132247
- import { EventEmitter as EventEmitter20 } from "node:events";
132948
+ import { EventEmitter as EventEmitter21 } from "node:events";
132248
132949
  var IpcHost;
132249
132950
  var init_ipc_host = __esm({
132250
132951
  "../engine/src/ipc/ipc-host.ts"() {
132251
132952
  "use strict";
132252
132953
  init_ipc_protocol();
132253
132954
  init_logger2();
132254
- IpcHost = class extends EventEmitter20 {
132955
+ IpcHost = class extends EventEmitter21 {
132255
132956
  /**
132256
132957
  * @param childProcess - The forked child process to communicate with
132257
132958
  * @param options - Optional configuration
@@ -132441,7 +133142,7 @@ var child_process_runtime_exports = {};
132441
133142
  __export(child_process_runtime_exports, {
132442
133143
  ChildProcessRuntime: () => ChildProcessRuntime
132443
133144
  });
132444
- import { EventEmitter as EventEmitter21 } from "node:events";
133145
+ import { EventEmitter as EventEmitter22 } from "node:events";
132445
133146
  import { fork } from "node:child_process";
132446
133147
  import { fileURLToPath as fileURLToPath3 } from "node:url";
132447
133148
  import { dirname as dirname18, join as join49 } from "node:path";
@@ -132498,8 +133199,8 @@ var init_child_process_runtime = __esm({
132498
133199
  runtimeLog.log("Health monitor stopped");
132499
133200
  }
132500
133201
  getRestartDelay() {
132501
- const delay4 = this.restartDelays[this.restartAttempts] ?? this.restartDelays[this.restartDelays.length - 1];
132502
- return delay4;
133202
+ const delay5 = this.restartDelays[this.restartAttempts] ?? this.restartDelays[this.restartDelays.length - 1];
133203
+ return delay5;
132503
133204
  }
132504
133205
  incrementRestartAttempts() {
132505
133206
  this.restartAttempts++;
@@ -132511,7 +133212,7 @@ var init_child_process_runtime = __esm({
132511
133212
  return this.missedHeartbeats;
132512
133213
  }
132513
133214
  };
132514
- ChildProcessRuntime = class extends EventEmitter21 {
133215
+ ChildProcessRuntime = class extends EventEmitter22 {
132515
133216
  /**
132516
133217
  * @param config - Runtime configuration
132517
133218
  * @param centralCore - CentralCore reference for global coordination
@@ -132780,9 +133481,9 @@ var init_child_process_runtime = __esm({
132780
133481
  this.emit("error", new Error("Child process failed after max restart attempts"));
132781
133482
  return;
132782
133483
  }
132783
- const delay4 = this.healthMonitor.getRestartDelay();
133484
+ const delay5 = this.healthMonitor.getRestartDelay();
132784
133485
  this.healthMonitor.incrementRestartAttempts();
132785
- runtimeLog.log(`Attempting restart ${this.healthMonitor.getRestartAttempts()}/${maxRestarts} after ${delay4}ms`);
133486
+ runtimeLog.log(`Attempting restart ${this.healthMonitor.getRestartAttempts()}/${maxRestarts} after ${delay5}ms`);
132786
133487
  const gen = this.generation;
132787
133488
  this.restartTimer = setTimeout(async () => {
132788
133489
  this.restartTimer = null;
@@ -132801,7 +133502,7 @@ var init_child_process_runtime = __esm({
132801
133502
  this.setStatus("errored");
132802
133503
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
132803
133504
  }
132804
- }, delay4);
133505
+ }, delay5);
132805
133506
  }
132806
133507
  /**
132807
133508
  * Handle child process exit.
@@ -133196,14 +133897,14 @@ var init_remote_node_client = __esm({
133196
133897
  });
133197
133898
 
133198
133899
  // ../engine/src/runtimes/remote-node-runtime.ts
133199
- import { EventEmitter as EventEmitter22 } from "node:events";
133900
+ import { EventEmitter as EventEmitter23 } from "node:events";
133200
133901
  var RemoteNodeRuntime;
133201
133902
  var init_remote_node_runtime = __esm({
133202
133903
  "../engine/src/runtimes/remote-node-runtime.ts"() {
133203
133904
  "use strict";
133204
133905
  init_logger2();
133205
133906
  init_remote_node_client();
133206
- RemoteNodeRuntime = class extends EventEmitter22 {
133907
+ RemoteNodeRuntime = class extends EventEmitter23 {
133207
133908
  constructor(config) {
133208
133909
  super();
133209
133910
  this.config = config;
@@ -133513,7 +134214,7 @@ var init_remote_node_runtime = __esm({
133513
134214
  });
133514
134215
 
133515
134216
  // ../engine/src/project-manager.ts
133516
- import { EventEmitter as EventEmitter23 } from "node:events";
134217
+ import { EventEmitter as EventEmitter24 } from "node:events";
133517
134218
  var ProjectManager;
133518
134219
  var init_project_manager = __esm({
133519
134220
  "../engine/src/project-manager.ts"() {
@@ -133523,7 +134224,7 @@ var init_project_manager = __esm({
133523
134224
  init_remote_node_runtime();
133524
134225
  init_concurrency();
133525
134226
  init_logger2();
133526
- ProjectManager = class extends EventEmitter23 {
134227
+ ProjectManager = class extends EventEmitter24 {
133527
134228
  /**
133528
134229
  * @param centralCore - CentralCore reference for global coordination
133529
134230
  */
@@ -133990,6 +134691,653 @@ var init_gridlock_detector = __esm({
133990
134691
  }
133991
134692
  });
133992
134693
 
134694
+ // ../engine/src/merger-ai.ts
134695
+ import { execFile as execFile15 } from "node:child_process";
134696
+ import { promisify as promisify43 } from "node:util";
134697
+ import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
134698
+ import { tmpdir as tmpdir5 } from "node:os";
134699
+ import { join as join50 } from "node:path";
134700
+ async function git2(args, cwd, opts = {}) {
134701
+ const { stdout } = await execFileAsync10("git", args, {
134702
+ cwd,
134703
+ encoding: "utf-8",
134704
+ timeout: opts.timeout ?? 12e4,
134705
+ maxBuffer: 16 * 1024 * 1024
134706
+ });
134707
+ return stdout.trim();
134708
+ }
134709
+ async function gitOk(args, cwd) {
134710
+ try {
134711
+ await git2(args, cwd);
134712
+ return true;
134713
+ } catch {
134714
+ return false;
134715
+ }
134716
+ }
134717
+ function taskTrailers(taskId, lineageId) {
134718
+ const trailers = [`${FUSION_TASK_ID_TRAILER_KEY3}: ${taskId}`];
134719
+ if (lineageId) trailers.push(buildTaskLineageTrailer(lineageId));
134720
+ return trailers;
134721
+ }
134722
+ async function ensureCommitTaskMetadata(mergeRoot, taskId, includeTaskId, trailers) {
134723
+ const fullMessage = await git2(["log", "-1", "--pretty=%B"], mergeRoot).catch(() => "");
134724
+ if (!fullMessage) return;
134725
+ const subject = (fullMessage.split("\n")[0] ?? "").trim();
134726
+ const body = await git2(["log", "-1", "--pretty=%b"], mergeRoot).catch(() => "");
134727
+ const needsPrefix = includeTaskId && !subject.toLowerCase().startsWith(taskId.toLowerCase());
134728
+ const missingTrailers = trailers.filter((t) => !fullMessage.includes(t));
134729
+ if (!needsPrefix && missingTrailers.length === 0) return;
134730
+ const args = ["-c", "trailer.ifExists=addIfDifferent", "commit", "--amend"];
134731
+ if (needsPrefix) {
134732
+ args.push("-m", `${taskId}: ${subject}`);
134733
+ if (body.trim()) args.push("-m", body);
134734
+ } else {
134735
+ args.push("--no-edit");
134736
+ }
134737
+ for (const t of missingTrailers) args.push("--trailer", t);
134738
+ await git2(args, mergeRoot).catch((err) => {
134739
+ aiMergeLog.warn(`failed to amend task metadata onto squash (${err instanceof Error ? err.message : String(err)})`);
134740
+ });
134741
+ }
134742
+ function parseReviewVerdict(agentText) {
134743
+ const text = (agentText ?? "").trim();
134744
+ if (!text) return { verdict: "reject", reasons: ["reviewer produced no output"], severity: "blocking" };
134745
+ const lines = text.split(/\r?\n/);
134746
+ let verdictLineIndex = -1;
134747
+ let decision = null;
134748
+ for (let i = lines.length - 1; i >= 0; i--) {
134749
+ const m2 = lines[i].match(VERDICT_LINE_RE);
134750
+ if (m2) {
134751
+ decision = m2[1].toLowerCase();
134752
+ verdictLineIndex = i;
134753
+ break;
134754
+ }
134755
+ }
134756
+ if (!decision) {
134757
+ return {
134758
+ verdict: "reject",
134759
+ reasons: [`reviewer did not emit a "${REVIEW_VERDICT_MARKER} approve|reject" line`],
134760
+ severity: "blocking"
134761
+ };
134762
+ }
134763
+ if (decision === "approve") return { verdict: "approve", reasons: [] };
134764
+ const severity = SEVERITY_LINE_RE.test(text) ? text.match(SEVERITY_LINE_RE)[1].toLowerCase() : "blocking";
134765
+ return { verdict: "reject", reasons: extractRejectReasons(lines, verdictLineIndex), severity };
134766
+ }
134767
+ function extractRejectReasons(lines, verdictLineIndex) {
134768
+ const reasons = [];
134769
+ const inline = lines[verdictLineIndex].replace(VERDICT_LINE_RE, "").replace(/^[\s:–—-]+/, "").trim();
134770
+ if (inline) reasons.push(inline);
134771
+ for (let i = verdictLineIndex + 1; i < lines.length; i++) {
134772
+ if (SEVERITY_LINE_RE.test(lines[i])) continue;
134773
+ const cleaned = lines[i].replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").trim();
134774
+ if (cleaned) reasons.push(cleaned);
134775
+ }
134776
+ if (reasons.length === 0) reasons.push("reviewer rejected the merge without a stated reason");
134777
+ return reasons;
134778
+ }
134779
+ function buildMergeSystemPrompt2(agentPrompts) {
134780
+ const base = resolveAgentPrompt("merger", agentPrompts).trim();
134781
+ return [
134782
+ base,
134783
+ base ? "" : void 0,
134784
+ "## AI merge \u2014 clean room",
134785
+ "You are on a CLEAN, detached checkout at the integration branch's current",
134786
+ "tip. Land the task branch's work as a single commit.",
134787
+ "",
134788
+ "Constraints:",
134789
+ " - Resolve every conflict in favor of the task branch's intent; never drop",
134790
+ " the task's changes to make a conflict go away.",
134791
+ " - Do not make edits unrelated to reconciling the two branches.",
134792
+ " - Do NOT push, force-push, or run `git update-ref` / `git reset --hard`",
134793
+ " on any other branch. Only commit on this detached HEAD.",
134794
+ " - Finish with exactly ONE new commit on HEAD containing the task's work.",
134795
+ "",
134796
+ "Verify before committing:",
134797
+ " - After resolving the merge, run the project's checks \u2014 tests, type-check,",
134798
+ " and lint (discover them from the project config / package.json scripts,",
134799
+ " e.g. test / typecheck / lint / build).",
134800
+ " - FIX any NEW failure the merge or conflict resolution introduced (a check",
134801
+ " that passed on the task branch or the integration tip but fails on the",
134802
+ " merged tree). You do not need to fix failures that were already broken on",
134803
+ " the integration branch beforehand, but never commit a merge that adds new",
134804
+ " test, type-check, or lint failures.",
134805
+ "",
134806
+ "Commit message:",
134807
+ " - The subject line must CONCISELY SUMMARIZE the squashed changes in",
134808
+ ' imperative mood (e.g. "add X", "fix Y") based on the actual diff \u2014 do',
134809
+ " not just restate the task title.",
134810
+ " - Include the task-id prefix and the trailer lines EXACTLY as given in the",
134811
+ " task instructions (they associate the commit with the board task)."
134812
+ ].filter((l) => l !== void 0).join("\n");
134813
+ }
134814
+ function buildMergePrompt2(input) {
134815
+ const subjectShape = input.includeTaskId ? `"${input.taskId}: <concise imperative summary of the squashed changes>"` : `"<concise imperative summary of the squashed changes>"`;
134816
+ const trailerArgs = input.trailers.map((t) => ` -m ${JSON.stringify(t)}`).join("");
134817
+ const lines = [
134818
+ `Merge branch "${input.branch}" into "${input.integrationBranch}" (HEAD is detached at ${short(input.tipSha)}).`,
134819
+ "",
134820
+ "Steps:",
134821
+ ` 1. Run: git merge --squash ${input.branch}`,
134822
+ " 2. If there are conflicts, resolve them (favor the task's intent), then `git add` the resolved files.",
134823
+ " 3. Commit the staged result as a SINGLE commit whose subject summarizes the",
134824
+ ` actual changes${input.taskTitle ? ` (task title hint: ${JSON.stringify(input.taskTitle)})` : ""}, including the required trailers:`,
134825
+ ` git commit -m ${subjectShape}${trailerArgs}`,
134826
+ " Keep the trailer line(s) verbatim \u2014 they link the commit to the board task.",
134827
+ " 4. Verify `git log --oneline ${tip}..HEAD` shows exactly one new commit and `git status` is clean.".replace("${tip}", short(input.tipSha)),
134828
+ "",
134829
+ "If `git merge --squash` reports the branch is already up to date (nothing to",
134830
+ "merge), do nothing and leave HEAD unchanged."
134831
+ ];
134832
+ if (input.correctiveReasons && input.correctiveReasons.length > 0) {
134833
+ lines.push(
134834
+ "",
134835
+ "A prior attempt was REJECTED by review. Redo the merge from the clean tip",
134836
+ "and address each of these problems:",
134837
+ ...input.correctiveReasons.map((r) => ` - ${r}`)
134838
+ );
134839
+ }
134840
+ return lines.join("\n");
134841
+ }
134842
+ function buildReviewSystemPrompt() {
134843
+ return [
134844
+ "You are an adversarial, read-only merge reviewer. Do NOT edit, stage, commit,",
134845
+ "or run any mutating git command. Audit the squash commit that is about to be",
134846
+ "merged into the integration branch and decide whether it is safe to land.",
134847
+ "",
134848
+ "Investigate with read-only commands (git show, git diff, git log, cat, grep).",
134849
+ "Judge on four axes:",
134850
+ " 1. Completeness \u2014 does the squash contain ALL of the task branch's intended",
134851
+ " changes? Flag any hunk silently dropped during conflict resolution.",
134852
+ " 2. No collateral \u2014 does it touch only files within the task's footprint?",
134853
+ " 3. Conflict soundness \u2014 were conflicts resolved coherently (both sides'",
134854
+ " intent preserved), not by blindly discarding one side?",
134855
+ " 4. Commit message \u2014 read `git show`'s message: the subject must concisely",
134856
+ " and ACCURATELY summarize the actual changes (not vague, not a mere",
134857
+ " restatement of the task title, not misleading). A poor/inaccurate",
134858
+ " message is an ADVISORY concern (it should be rewritten on retry, but",
134859
+ " must not block the merge).",
134860
+ "",
134861
+ "Bias toward rejection when uncertain.",
134862
+ "",
134863
+ `End with a single decision line: "${REVIEW_VERDICT_MARKER} approve" or`,
134864
+ `"${REVIEW_VERDICT_MARKER} reject". When rejecting, add a "SEVERITY:" line:`,
134865
+ " - SEVERITY: blocking \u2014 a correctness problem (dropped/lost task changes,",
134866
+ " incomplete squash, or a conflict resolution that discards intent). The",
134867
+ " merge must NOT land if this is unfixable.",
134868
+ " - SEVERITY: advisory \u2014 a quality/style concern that does not risk",
134869
+ " correctness; acceptable to land if unresolved.",
134870
+ "Then list each concrete reason as a bullet."
134871
+ ].join("\n");
134872
+ }
134873
+ function buildReviewPrompt(input) {
134874
+ const lines = [
134875
+ `Review the squash merge for task ${input.taskId} (branch ${input.branch} \u2192 ${input.integrationBranch}).`,
134876
+ "",
134877
+ `Integration tip: ${short(input.tipSha)}`,
134878
+ `Squash commit: ${short(input.squashSha)}`,
134879
+ "",
134880
+ "Inspect with:",
134881
+ ` git show ${input.squashSha}`,
134882
+ ` git diff ${input.tipSha}..${input.squashSha}`,
134883
+ "",
134884
+ "Files changed (git diff --stat):",
134885
+ input.diffStat.trim() || "(none reported)"
134886
+ ];
134887
+ if (input.priorReasons && input.priorReasons.length > 0) {
134888
+ lines.push(
134889
+ "",
134890
+ "A prior pass rejected an earlier attempt for these reasons \u2014 confirm they",
134891
+ "are now resolved:",
134892
+ ...input.priorReasons.map((r) => ` - ${r}`)
134893
+ );
134894
+ }
134895
+ return lines.join("\n");
134896
+ }
134897
+ function buildStashResolveSystemPrompt() {
134898
+ return [
134899
+ "You are resolving a conflict between the user's restored local working-tree",
134900
+ "edits and the freshly-merged integration branch. The user's uncommitted work",
134901
+ "was stashed, the checkout fast-forwarded to the new tip, and re-applying the",
134902
+ "stash produced conflicts.",
134903
+ "",
134904
+ "Resolve every conflict marker so BOTH sides are preserved: keep the user's",
134905
+ "local intent AND the upstream changes that just landed. Stage each resolved",
134906
+ "file with `git add`.",
134907
+ "",
134908
+ "Do NOT commit, stash, reset, checkout a different branch, or run update-ref.",
134909
+ "Leave the resolved changes in the working tree as the user's uncommitted edits."
134910
+ ].join("\n");
134911
+ }
134912
+ function buildStashResolvePrompt(conflictedFiles) {
134913
+ return [
134914
+ "Re-applying your stashed local changes onto the updated branch conflicted.",
134915
+ "",
134916
+ "Conflicted files:",
134917
+ ...conflictedFiles.map((f2) => ` - ${f2}`),
134918
+ "",
134919
+ "Resolve each file's conflict markers (preserve both the local edits and the",
134920
+ "upstream changes), then `git add` it. Do not commit."
134921
+ ].join("\n");
134922
+ }
134923
+ function short(sha) {
134924
+ return /^[0-9a-f]{7,40}$/i.test(sha) ? sha.slice(0, 8) : sha;
134925
+ }
134926
+ function makeMutatingAgent(store, settings, taskId, options, audit, systemPrompt) {
134927
+ return async (cwd, prompt) => {
134928
+ const model = resolveMergerSessionModel(settings);
134929
+ const { session } = await createResolvedAgentSession({
134930
+ sessionPurpose: "merger",
134931
+ pluginRunner: options.pluginRunner,
134932
+ cwd,
134933
+ systemPrompt,
134934
+ tools: "coding",
134935
+ onText: options.onAgentText ? (delta) => options.onAgentText?.(delta) : void 0,
134936
+ defaultProvider: model.provider,
134937
+ defaultModelId: model.modelId,
134938
+ fallbackProvider: settings.fallbackProvider,
134939
+ fallbackModelId: settings.fallbackModelId,
134940
+ defaultThinkingLevel: settings.defaultThinkingLevel,
134941
+ runAuditor: audit,
134942
+ settings,
134943
+ taskId
134944
+ });
134945
+ options.onSession?.(session);
134946
+ try {
134947
+ await withRateLimitRetry(async () => {
134948
+ await promptWithFallback(session, prompt);
134949
+ checkSessionError(session);
134950
+ }, { signal: options.signal });
134951
+ await accumulateSessionTokenUsage(store, taskId, session);
134952
+ } finally {
134953
+ session.dispose();
134954
+ }
134955
+ };
134956
+ }
134957
+ function makeReviewAgent(store, settings, taskId, options, audit) {
134958
+ return async (cwd, prompt) => {
134959
+ const validator = resolveValidatorSettingsModel(settings);
134960
+ const model = validator.provider && validator.modelId ? validator : resolveMergerSessionModel(settings);
134961
+ let captured = "";
134962
+ const { session } = await createResolvedAgentSession({
134963
+ sessionPurpose: "merger",
134964
+ pluginRunner: options.pluginRunner,
134965
+ cwd,
134966
+ systemPrompt: buildReviewSystemPrompt(),
134967
+ tools: "coding",
134968
+ onText: (delta) => {
134969
+ captured += delta;
134970
+ options.onAgentText?.(delta);
134971
+ },
134972
+ defaultProvider: model.provider,
134973
+ defaultModelId: model.modelId,
134974
+ fallbackProvider: settings.fallbackProvider,
134975
+ fallbackModelId: settings.fallbackModelId,
134976
+ defaultThinkingLevel: settings.defaultThinkingLevel,
134977
+ runAuditor: audit,
134978
+ settings,
134979
+ taskId
134980
+ });
134981
+ options.onSession?.(session);
134982
+ try {
134983
+ await withRateLimitRetry(async () => {
134984
+ await promptWithFallback(session, prompt);
134985
+ checkSessionError(session);
134986
+ }, { signal: options.signal });
134987
+ await accumulateSessionTokenUsage(store, taskId, session);
134988
+ } finally {
134989
+ session.dispose();
134990
+ }
134991
+ return captured;
134992
+ };
134993
+ }
134994
+ async function hasUnresolvedConflicts(cwd) {
134995
+ return (await git2(["ls-files", "-u"], cwd)).length > 0;
134996
+ }
134997
+ async function landSquash(input) {
134998
+ const { projectRootDir, mergeRoot, integrationBranch, tipSha, squashSha, taskId, audit, resolveConflicts: resolveConflicts2 } = input;
134999
+ const emit2 = (outcome, extra = {}) => audit.git({ type: "merge:ai-local-sync", target: integrationBranch, metadata: { taskId, outcome, squashSha, ...extra } }).catch(() => void 0);
135000
+ const currentBranch2 = await git2(["rev-parse", "--abbrev-ref", "HEAD"], projectRootDir).catch(() => "");
135001
+ if (currentBranch2 !== integrationBranch) {
135002
+ const adv = await advanceIntegrationBranchRef({
135003
+ rootDir: mergeRoot,
135004
+ projectRootDir,
135005
+ integrationBranch,
135006
+ newSha: squashSha,
135007
+ expectedCurrentSha: tipSha,
135008
+ taskId,
135009
+ audit
135010
+ });
135011
+ if (!adv.advanced) {
135012
+ if (adv.reason === "concurrent-advance" || adv.reason === "non-fast-forward-advance") {
135013
+ return { outcome: "concurrent", localSync: "skipped-other-branch" };
135014
+ }
135015
+ throw new Error(`AI merge could not advance ${integrationBranch} for ${taskId}: ${adv.reason} (${adv.diagnostic})`);
135016
+ }
135017
+ await emit2("skipped-other-branch", { currentBranch: currentBranch2 });
135018
+ return { outcome: "advanced", localSync: "skipped-other-branch" };
135019
+ }
135020
+ const head = await git2(["rev-parse", "HEAD"], projectRootDir).catch(() => "");
135021
+ if (head !== tipSha) {
135022
+ return { outcome: "concurrent", localSync: "skipped-other-branch" };
135023
+ }
135024
+ const dirty = (await git2(["status", "--porcelain"], projectRootDir)).length > 0;
135025
+ const stashed = dirty ? await gitOk(["stash", "push", "--include-untracked", "-m", `fusion-ai-merge-sync-${taskId}`], projectRootDir) : false;
135026
+ if (dirty && !stashed) {
135027
+ const adv = await advanceIntegrationBranchRef({
135028
+ rootDir: mergeRoot,
135029
+ projectRootDir,
135030
+ integrationBranch,
135031
+ newSha: squashSha,
135032
+ expectedCurrentSha: tipSha,
135033
+ taskId,
135034
+ audit
135035
+ });
135036
+ if (!adv.advanced) {
135037
+ if (adv.reason === "concurrent-advance" || adv.reason === "non-fast-forward-advance") {
135038
+ return { outcome: "concurrent", localSync: "skipped-dirty-unstashable" };
135039
+ }
135040
+ throw new Error(`AI merge could not advance ${integrationBranch} for ${taskId}: ${adv.reason} (${adv.diagnostic})`);
135041
+ }
135042
+ aiMergeLog.warn(`${taskId}: local checkout has un-stashable dirty state \u2014 advanced ${integrationBranch} without syncing your working tree; pull manually.`);
135043
+ await emit2("skipped-dirty-unstashable");
135044
+ return { outcome: "advanced", localSync: "skipped-dirty-unstashable" };
135045
+ }
135046
+ if (!await gitOk(["merge", "--ff-only", squashSha], projectRootDir)) {
135047
+ if (stashed) await gitOk(["stash", "pop"], projectRootDir);
135048
+ return { outcome: "concurrent", localSync: "skipped-other-branch" };
135049
+ }
135050
+ if (!stashed) {
135051
+ await emit2("ff");
135052
+ return { outcome: "advanced", localSync: "ff" };
135053
+ }
135054
+ if (await gitOk(["stash", "pop"], projectRootDir)) {
135055
+ await emit2("stash-ff-restore");
135056
+ return { outcome: "advanced", localSync: "stash-ff-restore" };
135057
+ }
135058
+ if (resolveConflicts2) {
135059
+ const conflicted = (await git2(["diff", "--name-only", "--diff-filter=U"], projectRootDir)).split("\n").map((l) => l.trim()).filter(Boolean);
135060
+ try {
135061
+ await resolveConflicts2(projectRootDir, buildStashResolvePrompt(conflicted));
135062
+ } catch (err) {
135063
+ aiMergeLog.warn(`${taskId}: AI stash-conflict resolver threw: ${err instanceof Error ? err.message : String(err)}`);
135064
+ }
135065
+ if (!await hasUnresolvedConflicts(projectRootDir)) {
135066
+ await gitOk(["reset"], projectRootDir);
135067
+ 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\`).`);
135068
+ await emit2("stash-ff-airesolved", { conflicted, stashRetained: true });
135069
+ return { outcome: "advanced", localSync: "stash-ff-airesolved" };
135070
+ }
135071
+ }
135072
+ 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.`);
135073
+ await emit2("stash-ff-conflict");
135074
+ return { outcome: "advanced", localSync: "stash-ff-conflict" };
135075
+ }
135076
+ function noOpResult(task, branch, reason) {
135077
+ return {
135078
+ task,
135079
+ branch,
135080
+ merged: false,
135081
+ noOp: true,
135082
+ ok: true,
135083
+ reason,
135084
+ worktreeRemoved: false,
135085
+ branchDeleted: false
135086
+ };
135087
+ }
135088
+ async function runAiMerge(store, projectRootDir, taskId, options = {}, deps = {}) {
135089
+ const task = await store.getTask(taskId);
135090
+ const branch = task.branch || canonicalFusionBranchName(taskId);
135091
+ if (task.column === "done" || task.column === "archived") {
135092
+ return noOpResult(task, branch, "already-finalized");
135093
+ }
135094
+ const blocker = getTaskMergeBlocker(task, { manual: options.manual === true });
135095
+ if (blocker) throw new Error(`Cannot merge ${taskId}: ${blocker}`);
135096
+ const settings = await store.getSettings();
135097
+ const projectDefaultBranch = await resolveIntegrationBranch(projectRootDir, settings);
135098
+ const mergeTarget = resolveTaskMergeTarget(task, { projectDefaultBranch });
135099
+ const integrationBranch = mergeTarget.branch;
135100
+ const audit = createRunAuditor(store, {
135101
+ runId: generateSyntheticRunId("ai-merge", taskId),
135102
+ agentId: "merger",
135103
+ taskId,
135104
+ phase: "merge"
135105
+ });
135106
+ const log25 = async (message) => {
135107
+ await store.logEntry(taskId, message, "AiMerge").catch(() => void 0);
135108
+ await store.appendAgentLog(taskId, message, "text", void 0, "merger").catch(() => void 0);
135109
+ };
135110
+ const setStatus = (status) => store.updateTask(taskId, { status }).catch(() => void 0);
135111
+ if (!await gitOk(["rev-parse", "--verify", `refs/heads/${branch}`], projectRootDir)) {
135112
+ const wasExecuted = !!task.baseCommitSha;
135113
+ const alreadyMerged = task.mergeDetails?.mergeConfirmed === true || !!task.mergeDetails?.commitSha;
135114
+ if (wasExecuted && !alreadyMerged) {
135115
+ await audit.git({
135116
+ type: "merge:ai-no-branch",
135117
+ target: branch,
135118
+ metadata: { taskId, kind: "executed-branch-missing", baseCommitSha: task.baseCommitSha }
135119
+ });
135120
+ throw new Error(
135121
+ `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.`
135122
+ );
135123
+ }
135124
+ await audit.git({
135125
+ type: "merge:ai-no-branch",
135126
+ target: branch,
135127
+ metadata: { taskId, kind: alreadyMerged ? "already-merged" : "never-executed" }
135128
+ });
135129
+ return await finalizeTask(store, taskId, noOpResult(task, branch, alreadyMerged ? "already-merged" : "no-branch"));
135130
+ }
135131
+ if (!await gitOk(["rev-parse", "--verify", `refs/heads/${integrationBranch}`], projectRootDir)) {
135132
+ await audit.git({ type: "merge:ai-no-branch", target: integrationBranch, metadata: { taskId, kind: "integration-branch-missing" } });
135133
+ 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.`);
135134
+ }
135135
+ const maxPasses = Math.max(0, Math.trunc(settings.merger?.maxReviewPasses ?? 3));
135136
+ const mergeAgent = deps.mergeAgent ?? makeMutatingAgent(store, settings, taskId, options, audit, buildMergeSystemPrompt2(settings.agentPrompts));
135137
+ const reviewAgent = deps.reviewAgent ?? makeReviewAgent(store, settings, taskId, options, audit);
135138
+ const stashResolveAgent = deps.stashResolveAgent ?? makeMutatingAgent(store, settings, taskId, options, audit, buildStashResolveSystemPrompt());
135139
+ const includeTaskId = settings.includeTaskIdInCommit !== false;
135140
+ const trailers = taskTrailers(taskId, task.lineageId);
135141
+ const taskTitle = task.title?.trim() ? task.title.split("\n")[0] : void 0;
135142
+ await setStatus("merging");
135143
+ let advanceRetries = 0;
135144
+ while (true) {
135145
+ throwIfAborted2(options.signal, taskId);
135146
+ const tipSha = await git2(["rev-parse", "--verify", `refs/heads/${integrationBranch}`], projectRootDir);
135147
+ const mergeRoot = await mkdtemp2(join50(tmpdir5(), `fusion-ai-merge-${taskId.toLowerCase()}-`));
135148
+ let worktreeAdded = false;
135149
+ try {
135150
+ await git2(["worktree", "add", "--detach", mergeRoot, tipSha], projectRootDir);
135151
+ worktreeAdded = true;
135152
+ await audit.git({ type: "merge:ai-clean-room", target: integrationBranch, metadata: { taskId, tipSha, mergeRoot } });
135153
+ await log25(`AI merge: merging ${branch} into ${integrationBranch} (clean room at ${short(tipSha)})${advanceRetries ? ` \u2014 retry ${advanceRetries} after concurrent advance` : ""}`);
135154
+ const squashSha = await mergeAndReview({
135155
+ mergeRoot,
135156
+ branch,
135157
+ integrationBranch,
135158
+ tipSha,
135159
+ taskTitle,
135160
+ includeTaskId,
135161
+ trailers,
135162
+ taskId,
135163
+ maxPasses,
135164
+ mergeAgent,
135165
+ reviewAgent,
135166
+ audit,
135167
+ log: log25,
135168
+ setStatus,
135169
+ signal: options.signal
135170
+ });
135171
+ if (!squashSha) {
135172
+ await audit.git({ type: "merge:ai-empty", target: integrationBranch, metadata: { taskId, tipSha } });
135173
+ await log25(`AI merge: ${branch} had no net changes vs ${integrationBranch} \u2014 finalizing as no-op`);
135174
+ return await finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, tipSha, audit, log25, { empty: true });
135175
+ }
135176
+ await setStatus("landing");
135177
+ const landed = await landSquash({
135178
+ projectRootDir,
135179
+ mergeRoot,
135180
+ integrationBranch,
135181
+ tipSha,
135182
+ squashSha,
135183
+ taskId,
135184
+ audit,
135185
+ resolveConflicts: stashResolveAgent
135186
+ });
135187
+ if (landed.outcome === "concurrent") {
135188
+ if (advanceRetries < MAX_CONCURRENT_ADVANCE_RETRIES) {
135189
+ advanceRetries++;
135190
+ await log25(`AI merge: ${integrationBranch} moved during merge \u2014 rebuilding on new tip (retry ${advanceRetries})`);
135191
+ continue;
135192
+ }
135193
+ throw new Error(`AI merge could not advance ${integrationBranch} for ${taskId} after ${advanceRetries} retries (concurrent advances)`);
135194
+ }
135195
+ await log25(`AI merge: advanced ${integrationBranch} \u2192 ${short(squashSha)} (local checkout: ${landed.localSync})`);
135196
+ return await finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, squashSha, audit, log25, { empty: false });
135197
+ } finally {
135198
+ if (worktreeAdded) {
135199
+ await gitOk(["worktree", "remove", "--force", mergeRoot], projectRootDir);
135200
+ }
135201
+ await rm6(mergeRoot, { recursive: true, force: true }).catch(() => void 0);
135202
+ }
135203
+ }
135204
+ }
135205
+ async function mergeAndReview(input) {
135206
+ const { mergeRoot, branch, integrationBranch, tipSha, taskTitle, includeTaskId, trailers, taskId, maxPasses, mergeAgent, reviewAgent, audit, log: log25, setStatus, signal } = input;
135207
+ let priorReasons = [];
135208
+ for (let attempt = 0; ; attempt++) {
135209
+ throwIfAborted2(signal, taskId);
135210
+ await git2(["reset", "--hard", tipSha], mergeRoot);
135211
+ await git2(["clean", "-fd"], mergeRoot);
135212
+ if (attempt > 0) {
135213
+ await setStatus("merging");
135214
+ await log25(`AI merge: corrective re-merge (pass ${attempt}/${maxPasses}) addressing: ${priorReasons.join("; ")}`);
135215
+ }
135216
+ await mergeAgent(mergeRoot, buildMergePrompt2({
135217
+ taskId,
135218
+ branch,
135219
+ integrationBranch,
135220
+ tipSha,
135221
+ taskTitle,
135222
+ includeTaskId,
135223
+ trailers,
135224
+ correctiveReasons: priorReasons.length ? priorReasons : void 0
135225
+ }));
135226
+ let head = await git2(["rev-parse", "HEAD"], mergeRoot);
135227
+ if (head === tipSha) return null;
135228
+ await ensureCommitTaskMetadata(mergeRoot, taskId, includeTaskId, trailers);
135229
+ head = await git2(["rev-parse", "HEAD"], mergeRoot);
135230
+ await setStatus("reviewing");
135231
+ const diffStat = await git2(["diff", "--stat", `${tipSha}..${head}`], mergeRoot);
135232
+ const verdict = parseReviewVerdict(await reviewAgent(mergeRoot, buildReviewPrompt({
135233
+ taskId,
135234
+ branch,
135235
+ integrationBranch,
135236
+ tipSha,
135237
+ squashSha: head,
135238
+ diffStat,
135239
+ priorReasons
135240
+ })));
135241
+ await audit.git({
135242
+ type: "merge:ai-review-verdict",
135243
+ target: integrationBranch,
135244
+ metadata: { taskId, attempt, verdict: verdict.verdict, severity: verdict.severity, reasons: verdict.reasons, squashSha: head }
135245
+ });
135246
+ if (verdict.verdict === "approve") {
135247
+ await log25(`AI merge review (pass ${attempt + 1}): approved`);
135248
+ return head;
135249
+ }
135250
+ const budgetExhausted = attempt >= maxPasses;
135251
+ if (budgetExhausted) {
135252
+ if (verdict.severity === "blocking") {
135253
+ await audit.git({ type: "merge:ai-review-blocked", target: integrationBranch, metadata: { taskId, attempt, reasons: verdict.reasons } });
135254
+ await log25(`AI merge BLOCKED after ${attempt} corrective pass(es) \u2014 unresolved correctness concern: ${verdict.reasons.join("; ")}`);
135255
+ throw new AiMergeBlockedError(taskId, verdict.reasons);
135256
+ }
135257
+ await audit.git({ type: "merge:ai-review-landed-with-concerns", target: integrationBranch, metadata: { taskId, attempt, reasons: verdict.reasons, squashSha: head } });
135258
+ await log25(`AI merge: landing with unresolved advisory concern(s): ${verdict.reasons.join("; ")}`);
135259
+ return head;
135260
+ }
135261
+ priorReasons = verdict.reasons;
135262
+ await log25(`AI merge review (pass ${attempt + 1}): rejected (${verdict.severity}) \u2014 ${verdict.reasons.join("; ")}`);
135263
+ }
135264
+ }
135265
+ async function finalizeMerged(store, projectRootDir, taskId, task, branch, integrationBranch, landedSha, audit, log25, opts) {
135266
+ let branchDeleted = false;
135267
+ if (branch !== integrationBranch && await gitOk(["branch", "-D", branch], projectRootDir)) {
135268
+ branchDeleted = true;
135269
+ await audit.git({ type: "branch:delete", target: branch, metadata: { taskId, force: true } }).catch(() => void 0);
135270
+ }
135271
+ let worktreeRemoved = false;
135272
+ if (task.worktree) {
135273
+ worktreeRemoved = await gitOk(["worktree", "remove", "--force", task.worktree], projectRootDir);
135274
+ await store.updateTask(taskId, { worktree: null }).catch(() => void 0);
135275
+ }
135276
+ const result = {
135277
+ task,
135278
+ branch,
135279
+ merged: !opts.empty,
135280
+ noOp: opts.empty,
135281
+ ok: true,
135282
+ reason: opts.empty ? "no-net-changes" : void 0,
135283
+ commitSha: opts.empty ? void 0 : landedSha,
135284
+ mergeConfirmed: !opts.empty,
135285
+ worktreeRemoved,
135286
+ branchDeleted
135287
+ };
135288
+ await audit.git({ type: "merge:ai-landed", target: integrationBranch, metadata: { taskId, landedSha, empty: opts.empty } }).catch(() => void 0);
135289
+ await log25(opts.empty ? `AI merge: finalized ${taskId} (no-op) \u2192 done` : `AI merge: landed ${short(landedSha)}, task \u2192 done`);
135290
+ return await finalizeTask(store, taskId, result);
135291
+ }
135292
+ async function finalizeTask(store, taskId, result) {
135293
+ await store.updateTask(taskId, { status: null }).catch(() => void 0);
135294
+ const task = await store.moveTask(taskId, "done");
135295
+ result.task = task;
135296
+ store.emit("task:merged", result);
135297
+ return result;
135298
+ }
135299
+ function throwIfAborted2(signal, taskId) {
135300
+ if (signal?.aborted) {
135301
+ const err = new Error(`AI merge aborted for ${taskId}`);
135302
+ err.name = "MergeAbortedError";
135303
+ throw err;
135304
+ }
135305
+ }
135306
+ var execFileAsync10, aiMergeLog, MAX_CONCURRENT_ADVANCE_RETRIES, FUSION_TASK_ID_TRAILER_KEY3, REVIEW_VERDICT_MARKER, VERDICT_LINE_RE, SEVERITY_LINE_RE, AiMergeBlockedError;
135307
+ var init_merger_ai = __esm({
135308
+ "../engine/src/merger-ai.ts"() {
135309
+ "use strict";
135310
+ init_src();
135311
+ init_worktree_names();
135312
+ init_integration_branch();
135313
+ init_merger_ref_update_advance();
135314
+ init_agent_session_helpers();
135315
+ init_pi();
135316
+ init_rate_limit_retry();
135317
+ init_usage_limit_detector();
135318
+ init_session_token_usage();
135319
+ init_run_audit();
135320
+ init_logger2();
135321
+ execFileAsync10 = promisify43(execFile15);
135322
+ aiMergeLog = createLogger2("merger-ai");
135323
+ MAX_CONCURRENT_ADVANCE_RETRIES = 3;
135324
+ FUSION_TASK_ID_TRAILER_KEY3 = "Fusion-Task-Id";
135325
+ REVIEW_VERDICT_MARKER = "REVIEW_VERDICT:";
135326
+ VERDICT_LINE_RE = /REVIEW_VERDICT:\s*(approve|reject)\b/i;
135327
+ SEVERITY_LINE_RE = /SEVERITY:\s*(blocking|advisory)\b/i;
135328
+ AiMergeBlockedError = class extends Error {
135329
+ taskId;
135330
+ reasons;
135331
+ constructor(taskId, reasons) {
135332
+ super(`AI merge blocked ${taskId} (unresolved correctness concern): ${reasons.join("; ") || "no reason given"}`);
135333
+ this.name = "AiMergeBlockedError";
135334
+ this.taskId = taskId;
135335
+ this.reasons = reasons;
135336
+ }
135337
+ };
135338
+ }
135339
+ });
135340
+
133993
135341
  // ../engine/src/research-dispatcher.ts
133994
135342
  var log24, ResearchRunDispatcher;
133995
135343
  var init_research_dispatcher = __esm({
@@ -134232,9 +135580,9 @@ var init_provider_adapters = __esm({
134232
135580
  });
134233
135581
 
134234
135582
  // ../engine/src/remote-access/tunnel-process-manager.ts
134235
- import { EventEmitter as EventEmitter24 } from "node:events";
134236
- import { exec as exec36, execFile as execFile14, spawn as spawn8 } from "node:child_process";
134237
- import { promisify as promisify43 } from "node:util";
135583
+ import { EventEmitter as EventEmitter25 } from "node:events";
135584
+ import { exec as exec36, execFile as execFile16, spawn as spawn8 } from "node:child_process";
135585
+ import { promisify as promisify44 } from "node:util";
134238
135586
  function nowIso2() {
134239
135587
  return (/* @__PURE__ */ new Date()).toISOString();
134240
135588
  }
@@ -134274,7 +135622,7 @@ function toStateError(code, err) {
134274
135622
  at: nowIso2()
134275
135623
  };
134276
135624
  }
134277
- var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync10, execAsync32, LineBuffer, TunnelProcessManager;
135625
+ var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync11, execAsync32, LineBuffer, TunnelProcessManager;
134278
135626
  var init_tunnel_process_manager = __esm({
134279
135627
  "../engine/src/remote-access/tunnel-process-manager.ts"() {
134280
135628
  "use strict";
@@ -134283,8 +135631,8 @@ var init_tunnel_process_manager = __esm({
134283
135631
  init_provider_adapters();
134284
135632
  DEFAULT_MAX_LOG_ENTRIES = 400;
134285
135633
  DEFAULT_STOP_TIMEOUT_MS2 = 5e3;
134286
- execFileAsync10 = promisify43(execFile14);
134287
- execAsync32 = promisify43(exec36);
135634
+ execFileAsync11 = promisify44(execFile16);
135635
+ execAsync32 = promisify44(exec36);
134288
135636
  LineBuffer = class {
134289
135637
  pending = "";
134290
135638
  push(chunk) {
@@ -134299,7 +135647,7 @@ var init_tunnel_process_manager = __esm({
134299
135647
  return tail ? [tail] : [];
134300
135648
  }
134301
135649
  };
134302
- TunnelProcessManager = class extends EventEmitter24 {
135650
+ TunnelProcessManager = class extends EventEmitter25 {
134303
135651
  maxLogEntries;
134304
135652
  defaultStopTimeoutMs;
134305
135653
  spawnImpl;
@@ -134361,7 +135709,7 @@ var init_tunnel_process_manager = __esm({
134361
135709
  return null;
134362
135710
  }
134363
135711
  try {
134364
- const { stdout } = await execFileAsync10("tailscale", ["status", "--json"], { timeout: 3e3 });
135712
+ const { stdout } = await execFileAsync11("tailscale", ["status", "--json"], { timeout: 3e3 });
134365
135713
  const data = JSON.parse(String(stdout));
134366
135714
  const dnsName = data.Self?.DNSName?.replace(/\.$/, "");
134367
135715
  if (!dnsName) {
@@ -134384,7 +135732,7 @@ var init_tunnel_process_manager = __esm({
134384
135732
  ];
134385
135733
  for (const resetCommand of resetCommands) {
134386
135734
  try {
134387
- await execFileAsync10(resetCommand.command, resetCommand.args, { timeout: 5e3 });
135735
+ await execFileAsync11(resetCommand.command, resetCommand.args, { timeout: 5e3 });
134388
135736
  return;
134389
135737
  } catch {
134390
135738
  }
@@ -134685,8 +136033,8 @@ var init_tunnel_process_manager = __esm({
134685
136033
  });
134686
136034
 
134687
136035
  // ../engine/src/project-engine.ts
134688
- import { execFile as execFile15 } from "node:child_process";
134689
- import { promisify as promisify44 } from "node:util";
136036
+ import { execFile as execFile17 } from "node:child_process";
136037
+ import { promisify as promisify45 } from "node:util";
134690
136038
  function formatErrorDetails(error) {
134691
136039
  if (error instanceof Error) {
134692
136040
  return {
@@ -134701,6 +136049,40 @@ function isInvalidDoneTransitionError(error) {
134701
136049
  const message = error instanceof Error ? error.message : String(error);
134702
136050
  return message.includes("Invalid transition:") && message.includes("\u2192 'done'");
134703
136051
  }
136052
+ async function verifyMergeConfirmedReachability(args) {
136053
+ const { commitSha, integrationBranch, cwd } = args;
136054
+ if (!commitSha || !commitSha.trim()) {
136055
+ return { reachable: true, skipped: "no-commit-sha" };
136056
+ }
136057
+ if (!integrationBranch || !integrationBranch.trim()) {
136058
+ return { reachable: true, skipped: "no-integration-branch" };
136059
+ }
136060
+ try {
136061
+ await execFileAsync12("git", ["cat-file", "-e", `${commitSha}^{commit}`], {
136062
+ cwd,
136063
+ timeout: 1e4
136064
+ });
136065
+ } catch (error) {
136066
+ const diagnostic = error instanceof Error ? error.message : String(error);
136067
+ return { reachable: false, reason: "commit-missing", diagnostic };
136068
+ }
136069
+ try {
136070
+ await execFileAsync12(
136071
+ "git",
136072
+ ["merge-base", "--is-ancestor", commitSha, `refs/heads/${integrationBranch}`],
136073
+ { cwd, timeout: 1e4 }
136074
+ );
136075
+ return { reachable: true };
136076
+ } catch (error) {
136077
+ const err = error;
136078
+ const code = typeof err.code === "number" ? err.code : void 0;
136079
+ const diagnostic = err.message ?? String(error);
136080
+ if (code === 1) {
136081
+ return { reachable: false, reason: "not-ancestor", diagnostic };
136082
+ }
136083
+ return { reachable: false, reason: "git-error", diagnostic };
136084
+ }
136085
+ }
134704
136086
  function buildVerificationFailureSignature(error) {
134705
136087
  const commandResult = error.verificationResult.testResult ?? error.verificationResult.buildResult;
134706
136088
  const lane = commandResult?.command?.trim() || error.verificationResult.failedCommand?.trim() || "verification-failure";
@@ -134711,7 +136093,7 @@ function buildVerificationFailureSignature(error) {
134711
136093
  failedCommand: commandResult?.command ?? error.verificationResult.failedCommand ?? null
134712
136094
  }).signature;
134713
136095
  }
134714
- var execFileAsync11, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
136096
+ var execFileAsync12, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
134715
136097
  var init_project_engine = __esm({
134716
136098
  "../engine/src/project-engine.ts"() {
134717
136099
  "use strict";
@@ -134725,6 +136107,7 @@ var init_project_engine = __esm({
134725
136107
  init_auth_storage();
134726
136108
  init_cron_runner();
134727
136109
  init_merger();
136110
+ init_merger_ai();
134728
136111
  init_concurrency();
134729
136112
  init_logger2();
134730
136113
  init_research_orchestrator();
@@ -134733,7 +136116,7 @@ var init_project_engine = __esm({
134733
136116
  init_run_audit();
134734
136117
  init_verification_followup_dedup();
134735
136118
  init_tunnel_process_manager();
134736
- execFileAsync11 = promisify44(execFile15);
136119
+ execFileAsync12 = promisify45(execFile17);
134737
136120
  MERGE_HANDOFF_GRACE_MS = 300;
134738
136121
  isRemoteActive = (ra) => ra?.activeProvider != null && (ra.providers[ra.activeProvider]?.enabled ?? false);
134739
136122
  ProjectEngine = class _ProjectEngine {
@@ -134765,6 +136148,7 @@ var init_project_engine = __esm({
134765
136148
  notifier;
134766
136149
  notificationService;
134767
136150
  oauthExpiryMonitor;
136151
+ oauthValidityLogger;
134768
136152
  gridlockDetector;
134769
136153
  cronRunner;
134770
136154
  automationStore;
@@ -134878,11 +136262,14 @@ var init_project_engine = __esm({
134878
136262
  agentNameResolver
134879
136263
  });
134880
136264
  await this.notificationService.start();
136265
+ const authStorage = createFusionAuthStorage();
134881
136266
  this.oauthExpiryMonitor = new OAuthExpiryMonitor({
134882
- authStorage: createFusionAuthStorage(),
136267
+ authStorage,
134883
136268
  notificationService: this.notificationService
134884
136269
  });
134885
136270
  await this.oauthExpiryMonitor.start();
136271
+ this.oauthValidityLogger = new OAuthValidityLogger({ authStorage });
136272
+ await this.oauthValidityLogger.start();
134886
136273
  this.notifier = new NtfyNotifier(
134887
136274
  store,
134888
136275
  {
@@ -135066,6 +136453,7 @@ ${detail}`
135066
136453
  } catch {
135067
136454
  }
135068
136455
  this.oauthExpiryMonitor?.stop();
136456
+ this.oauthValidityLogger?.stop();
135069
136457
  this.notificationService?.stop();
135070
136458
  this.notifier?.stop();
135071
136459
  this.gridlockDetector?.stop();
@@ -135473,7 +136861,7 @@ ${detail}`
135473
136861
  async checkExecutableAvailable(command) {
135474
136862
  const checker = process.platform === "win32" ? "where" : "which";
135475
136863
  try {
135476
- await execFileAsync11(checker, [command]);
136864
+ await execFileAsync12(checker, [command]);
135477
136865
  return { available: true };
135478
136866
  } catch {
135479
136867
  return {
@@ -135673,6 +137061,112 @@ ${detail}`
135673
137061
  continue;
135674
137062
  }
135675
137063
  if (task.mergeDetails?.mergeConfirmed) {
137064
+ const integrationBranchForGate = task.mergeDetails.mergeTargetBranch || task.baseBranch || "main";
137065
+ const reachability = await verifyMergeConfirmedReachability({
137066
+ commitSha: task.mergeDetails.commitSha,
137067
+ integrationBranch: integrationBranchForGate,
137068
+ cwd
137069
+ });
137070
+ if (!reachability.reachable) {
137071
+ const sha = task.mergeDetails.commitSha || "";
137072
+ const shortSha = sha ? sha.slice(0, 8) : "<no-sha>";
137073
+ const currentRetries = task.mergeRetries ?? 0;
137074
+ const budgetExhausted = currentRetries >= _ProjectEngine.MAX_AUTO_MERGE_RETRIES;
137075
+ const cleanedMergeDetails = {
137076
+ ...task.mergeDetails,
137077
+ mergeConfirmed: false,
137078
+ commitSha: void 0,
137079
+ mergedAt: void 0,
137080
+ landedFiles: void 0,
137081
+ filesChanged: void 0,
137082
+ insertions: void 0,
137083
+ deletions: void 0,
137084
+ noOpVerifiedShortCircuit: void 0,
137085
+ landedFilesAttributionRestricted: void 0
137086
+ };
137087
+ if (budgetExhausted) {
137088
+ const errorMsg = `Auto-merge fast-path refused after ${currentRetries} attempts: commit ${shortSha} is not reachable from ${integrationBranchForGate} (${reachability.reason}). Manual review required.`;
137089
+ runtimeLog.warn(
137090
+ `Auto-merge: ${taskId} fast-path REFUSED + budget exhausted \u2014 ${reachability.reason}: ${reachability.diagnostic}`
137091
+ );
137092
+ await store.logEntry(
137093
+ taskId,
137094
+ `[FN-5627] Auto-merge fast-path refused (retry budget exhausted) \u2014 ${errorMsg}`
137095
+ );
137096
+ await store.updateTask(taskId, {
137097
+ mergeDetails: cleanedMergeDetails,
137098
+ status: "failed",
137099
+ error: errorMsg
137100
+ });
137101
+ try {
137102
+ const auditor = createRunAuditor(store, {
137103
+ runId: generateSyntheticRunId("merger-fast-path-refused", taskId),
137104
+ agentId: "merger",
137105
+ taskId,
137106
+ phase: "auto-merge-fast-path-gate"
137107
+ });
137108
+ await auditor.database({
137109
+ type: "merger:fast-path-blocked-foreign-commit",
137110
+ target: taskId,
137111
+ metadata: {
137112
+ taskId,
137113
+ commitSha: sha,
137114
+ integrationBranch: integrationBranchForGate,
137115
+ reason: reachability.reason,
137116
+ diagnostic: reachability.diagnostic,
137117
+ mergeRetries: currentRetries,
137118
+ budgetExhausted: true
137119
+ }
137120
+ });
137121
+ } catch (auditErr) {
137122
+ runtimeLog.warn(
137123
+ `Auto-merge: ${taskId} fast-path audit emit failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
137124
+ );
137125
+ }
137126
+ continue;
137127
+ }
137128
+ const nextRetries = currentRetries + 1;
137129
+ runtimeLog.warn(
137130
+ `Auto-merge: ${taskId} fast-path REFUSED \u2014 auto-recovering (attempt ${nextRetries}/${_ProjectEngine.MAX_AUTO_MERGE_RETRIES}): ${reachability.reason}: ${reachability.diagnostic}`
137131
+ );
137132
+ await store.logEntry(
137133
+ taskId,
137134
+ `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].`
137135
+ );
137136
+ await store.updateTask(taskId, {
137137
+ mergeDetails: cleanedMergeDetails,
137138
+ mergeRetries: nextRetries,
137139
+ status: null,
137140
+ error: null
137141
+ });
137142
+ try {
137143
+ const auditor = createRunAuditor(store, {
137144
+ runId: generateSyntheticRunId("merger-fast-path-auto-recovered", taskId),
137145
+ agentId: "merger",
137146
+ taskId,
137147
+ phase: "auto-merge-fast-path-gate"
137148
+ });
137149
+ await auditor.database({
137150
+ type: "merger:fast-path-auto-recovered",
137151
+ target: taskId,
137152
+ metadata: {
137153
+ taskId,
137154
+ commitSha: sha,
137155
+ integrationBranch: integrationBranchForGate,
137156
+ reason: reachability.reason,
137157
+ diagnostic: reachability.diagnostic,
137158
+ mergeRetries: nextRetries,
137159
+ maxRetries: _ProjectEngine.MAX_AUTO_MERGE_RETRIES
137160
+ }
137161
+ });
137162
+ } catch (auditErr) {
137163
+ runtimeLog.warn(
137164
+ `Auto-merge: ${taskId} fast-path audit emit failed: ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
137165
+ );
137166
+ }
137167
+ this.internalEnqueueMerge(taskId);
137168
+ continue;
137169
+ }
135676
137170
  const blockerReason = getTaskHardMergeBlocker(task);
135677
137171
  if (blockerReason) {
135678
137172
  await store.updateTask(taskId, {
@@ -135808,10 +137302,10 @@ ${detail}`
135808
137302
  const pool = this.runtime.worktreePool;
135809
137303
  const agentStore = this.runtime.agentStore;
135810
137304
  const usageLimitPauser = this.runtime.usageLimitPauser;
135811
- const rawMerge = () => {
137305
+ const rawMerge = async () => {
135812
137306
  this.activeMergeTaskId = taskId;
135813
137307
  this.mergeAbortController = new AbortController();
135814
- return aiMergeTask(store, cwd, taskId, {
137308
+ const mergerOptions = {
135815
137309
  manual: !!manualResolver,
135816
137310
  pool,
135817
137311
  usageLimitPauser,
@@ -135820,7 +137314,9 @@ ${detail}`
135820
137314
  onSession: (session) => {
135821
137315
  this.activeMergeSession = session;
135822
137316
  }
135823
- });
137317
+ };
137318
+ const mergerMode = normalizeMergerMode((await store.getSettings().catch(() => ({}))).merger?.mode);
137319
+ return mergerMode === "ai" ? runAiMerge(store, cwd, taskId, mergerOptions) : aiMergeTask(store, cwd, taskId, mergerOptions);
135824
137320
  };
135825
137321
  let result;
135826
137322
  if (semaphore) {
@@ -138376,8 +139872,8 @@ var require_proper_lockfile = __commonJS({
138376
139872
  import { mkdirSync as mkdirSync10 } from "node:fs";
138377
139873
  import { access as access7, unlink as unlink7, writeFile as writeFile18 } from "node:fs/promises";
138378
139874
  import net from "node:net";
138379
- import { platform as platform3, tmpdir as tmpdir5 } from "node:os";
138380
- import { join as join50 } from "node:path";
139875
+ import { platform as platform3, tmpdir as tmpdir6 } from "node:os";
139876
+ import { join as join51 } from "node:path";
138381
139877
  import { createHash as createHash14 } from "node:crypto";
138382
139878
  function projectHash(projectId) {
138383
139879
  return createHash14("sha1").update(projectId).digest("hex").slice(0, 16);
@@ -138387,10 +139883,10 @@ function computeEngineSocketPath(projectId) {
138387
139883
  if (platform3() === "win32") {
138388
139884
  return `\\\\.\\pipe\\fusion-engine-${hash}`;
138389
139885
  }
138390
- return join50(tmpdir5(), `fusion-engine-${hash}.sock`);
139886
+ return join51(tmpdir6(), `fusion-engine-${hash}.sock`);
138391
139887
  }
138392
139888
  function computeEngineLockFilePath(workingDir) {
138393
- return join50(workingDir, ".fusion", "engine.lock");
139889
+ return join51(workingDir, ".fusion", "engine.lock");
138394
139890
  }
138395
139891
  async function ensureLockTargetExists(lockPath) {
138396
139892
  try {
@@ -138400,7 +139896,7 @@ async function ensureLockTargetExists(lockPath) {
138400
139896
  }
138401
139897
  }
138402
139898
  async function acquireLockfile(workingDir, onCompromised) {
138403
- const dir = join50(workingDir, ".fusion");
139899
+ const dir = join51(workingDir, ".fusion");
138404
139900
  mkdirSync10(dir, { recursive: true });
138405
139901
  const path6 = computeEngineLockFilePath(workingDir);
138406
139902
  await ensureLockTargetExists(path6);
@@ -139016,7 +140512,7 @@ var init_node_health_monitor = __esm({
139016
140512
  });
139017
140513
 
139018
140514
  // ../engine/src/hybrid-executor.ts
139019
- import { EventEmitter as EventEmitter25 } from "node:events";
140515
+ import { EventEmitter as EventEmitter26 } from "node:events";
139020
140516
  var HybridExecutor;
139021
140517
  var init_hybrid_executor = __esm({
139022
140518
  "../engine/src/hybrid-executor.ts"() {
@@ -139024,7 +140520,7 @@ var init_hybrid_executor = __esm({
139024
140520
  init_project_manager();
139025
140521
  init_node_health_monitor();
139026
140522
  init_logger2();
139027
- HybridExecutor = class extends EventEmitter25 {
140523
+ HybridExecutor = class extends EventEmitter26 {
139028
140524
  /**
139029
140525
  * @param centralCore - CentralCore reference for global coordination
139030
140526
  * @param options - Optional configuration callbacks
@@ -140334,7 +141830,7 @@ var init_planning_board_tools = __esm({
140334
141830
 
140335
141831
  // ../dashboard/src/planning.ts
140336
141832
  import { randomUUID as randomUUID22 } from "node:crypto";
140337
- import { EventEmitter as EventEmitter26 } from "node:events";
141833
+ import { EventEmitter as EventEmitter27 } from "node:events";
140338
141834
  function ensureEngineReady() {
140339
141835
  return Promise.resolve();
140340
141836
  }
@@ -141358,7 +142854,7 @@ For completion:
141358
142854
  process.on("beforeExit", () => {
141359
142855
  clearInterval(cleanupInterval2);
141360
142856
  });
141361
- PlanningStreamManager = class extends EventEmitter26 {
142857
+ PlanningStreamManager = class extends EventEmitter27 {
141362
142858
  constructor(bufferSize = 100) {
141363
142859
  super();
141364
142860
  this.bufferSize = bufferSize;
@@ -141573,7 +143069,7 @@ var init_ai_session_timeout = __esm({
141573
143069
  });
141574
143070
 
141575
143071
  // ../dashboard/src/subtask-breakdown.ts
141576
- import { EventEmitter as EventEmitter27 } from "node:events";
143072
+ import { EventEmitter as EventEmitter28 } from "node:events";
141577
143073
  function cleanupInMemorySubtaskSession(sessionId) {
141578
143074
  const session = sessions2.get(sessionId);
141579
143075
  if (!session) {
@@ -141615,7 +143111,7 @@ var init_subtask_breakdown = __esm({
141615
143111
  process.on("beforeExit", () => {
141616
143112
  clearInterval(cleanupInterval3);
141617
143113
  });
141618
- SubtaskStreamManager = class extends EventEmitter27 {
143114
+ SubtaskStreamManager = class extends EventEmitter28 {
141619
143115
  constructor(bufferSize = 100) {
141620
143116
  super();
141621
143117
  this.bufferSize = bufferSize;
@@ -141677,7 +143173,7 @@ var init_subtask_breakdown = __esm({
141677
143173
  });
141678
143174
 
141679
143175
  // ../dashboard/src/mission-interview.ts
141680
- import { EventEmitter as EventEmitter28 } from "node:events";
143176
+ import { EventEmitter as EventEmitter29 } from "node:events";
141681
143177
  function cleanupInMemoryMissionSession(sessionId) {
141682
143178
  const session = sessions3.get(sessionId);
141683
143179
  if (!session) {
@@ -141728,7 +143224,7 @@ var init_mission_interview = __esm({
141728
143224
  cleanupInterval4 = setInterval(cleanupExpiredSessions3, CLEANUP_INTERVAL_MS4);
141729
143225
  cleanupInterval4.unref?.();
141730
143226
  process.on("beforeExit", () => clearInterval(cleanupInterval4));
141731
- MissionInterviewStreamManager = class extends EventEmitter28 {
143227
+ MissionInterviewStreamManager = class extends EventEmitter29 {
141732
143228
  constructor(bufferSize = 100) {
141733
143229
  super();
141734
143230
  this.bufferSize = bufferSize;
@@ -141796,7 +143292,7 @@ var init_mission_interview = __esm({
141796
143292
  });
141797
143293
 
141798
143294
  // ../dashboard/src/milestone-slice-interview.ts
141799
- import { EventEmitter as EventEmitter29 } from "node:events";
143295
+ import { EventEmitter as EventEmitter30 } from "node:events";
141800
143296
  function cleanupInMemorySession2(sessionId) {
141801
143297
  const session = sessions4.get(sessionId);
141802
143298
  if (!session) {
@@ -141848,7 +143344,7 @@ var init_milestone_slice_interview = __esm({
141848
143344
  cleanupInterval5 = setInterval(cleanupExpiredSessions4, CLEANUP_INTERVAL_MS5);
141849
143345
  cleanupInterval5.unref?.();
141850
143346
  process.on("beforeExit", () => clearInterval(cleanupInterval5));
141851
- MilestoneSliceInterviewStreamManager = class extends EventEmitter29 {
143347
+ MilestoneSliceInterviewStreamManager = class extends EventEmitter30 {
141852
143348
  constructor(bufferSize = 100) {
141853
143349
  super();
141854
143350
  this.bufferSize = bufferSize;
@@ -142163,13 +143659,13 @@ var init_cli_spawn = __esm({
142163
143659
  // ../../plugins/fusion-plugin-hermes-runtime/dist/fusion-skill-install.js
142164
143660
  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";
142165
143661
  import { homedir as homedir8 } from "node:os";
142166
- import { basename as basename14, dirname as dirname19, join as join51, resolve as resolve29 } from "node:path";
143662
+ import { basename as basename14, dirname as dirname19, join as join52, resolve as resolve29 } from "node:path";
142167
143663
  import { fileURLToPath as fileURLToPath4 } from "node:url";
142168
143664
  function resolveHermesHome(profile) {
142169
- const base = process.env.HERMES_HOME ?? join51(homedir8(), ".hermes");
143665
+ const base = process.env.HERMES_HOME ?? join52(homedir8(), ".hermes");
142170
143666
  if (!profile || profile === "default")
142171
143667
  return base;
142172
- return join51(base, "profiles", profile);
143668
+ return join52(base, "profiles", profile);
142173
143669
  }
142174
143670
  function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
142175
143671
  const here = fileURLToPath4(moduleUrl);
@@ -142184,14 +143680,14 @@ function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
142184
143680
  function resolveBundledFusionSkillSource() {
142185
143681
  const candidates = getFusionSkillSourceCandidates();
142186
143682
  for (const candidate of candidates) {
142187
- if (existsSync43(join51(candidate, "SKILL.md")))
143683
+ if (existsSync43(join52(candidate, "SKILL.md")))
142188
143684
  return candidate;
142189
143685
  }
142190
143686
  return null;
142191
143687
  }
142192
143688
  function installFusionSkillIntoHermesHome(options = {}) {
142193
143689
  const sourceDir = options.sourceDir ?? resolveBundledFusionSkillSource();
142194
- const targetDir = join51(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
143690
+ const targetDir = join52(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
142195
143691
  if (!sourceDir) {
142196
143692
  return {
142197
143693
  outcome: "warning",
@@ -142280,7 +143776,7 @@ function isBrokenSymlink(path6) {
142280
143776
  }
142281
143777
  }
142282
143778
  function looksLikePriorFusionInstall(path6) {
142283
- const skillMd = join51(path6, "SKILL.md");
143779
+ const skillMd = join52(path6, "SKILL.md");
142284
143780
  if (!existsSync43(skillMd))
142285
143781
  return false;
142286
143782
  try {
@@ -142295,7 +143791,7 @@ function looksLikeFusionSkillTarget(path6) {
142295
143791
  return false;
142296
143792
  if (basename14(path6).toLowerCase() === FUSION_SKILL_NAME)
142297
143793
  return true;
142298
- return existsSync43(join51(path6, "SKILL.md"));
143794
+ return existsSync43(join52(path6, "SKILL.md"));
142299
143795
  }
142300
143796
  var FUSION_SKILL_NAME;
142301
143797
  var init_fusion_skill_install = __esm({
@@ -142673,8 +144169,8 @@ var init_pi_module = __esm({
142673
144169
 
142674
144170
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/mcp-config.js
142675
144171
  import { writeFileSync as writeFileSync5 } from "node:fs";
142676
- import { tmpdir as tmpdir6 } from "node:os";
142677
- import { join as join52 } from "node:path";
144172
+ import { tmpdir as tmpdir7 } from "node:os";
144173
+ import { join as join53 } from "node:path";
142678
144174
  import { fileURLToPath as fileURLToPath5 } from "node:url";
142679
144175
  import { dirname as dirname20 } from "node:path";
142680
144176
  function toolsToMcpToolDefs(tools) {
@@ -142688,16 +144184,16 @@ function toolsToMcpToolDefs(tools) {
142688
144184
  }
142689
144185
  function writeOpenClawMcpBridgeFiles(toolDefs, cacheKey) {
142690
144186
  const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
142691
- const schemaPath = join52(tmpdir6(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
144187
+ const schemaPath = join53(tmpdir7(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
142692
144188
  writeFileSync5(schemaPath, JSON.stringify(toolDefs));
142693
144189
  const __filename = fileURLToPath5(import.meta.url);
142694
144190
  const __dirname2 = dirname20(__filename);
142695
- const serverPath = join52(__dirname2, "mcp-schema-server.cjs");
144191
+ const serverPath = join53(__dirname2, "mcp-schema-server.cjs");
142696
144192
  const serverConfig = {
142697
144193
  command: "node",
142698
144194
  args: [serverPath, schemaPath]
142699
144195
  };
142700
- const serverConfigPath = join52(tmpdir6(), `openclaw-runtime-mcp-server-${suffix}.json`);
144196
+ const serverConfigPath = join53(tmpdir7(), `openclaw-runtime-mcp-server-${suffix}.json`);
142701
144197
  writeFileSync5(serverConfigPath, JSON.stringify(serverConfig));
142702
144198
  return {
142703
144199
  schemaPath,
@@ -142942,10 +144438,10 @@ var init_context = __esm({
142942
144438
 
142943
144439
  // ../dashboard/src/github.ts
142944
144440
  import { exec as exec37 } from "node:child_process";
142945
- import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
142946
- import { tmpdir as tmpdir7 } from "node:os";
142947
- import { join as join53 } from "node:path";
142948
- import { promisify as promisify45 } from "node:util";
144441
+ import { mkdtemp as mkdtemp3, rm as rm7 } from "node:fs/promises";
144442
+ import { tmpdir as tmpdir8 } from "node:os";
144443
+ import { join as join54 } from "node:path";
144444
+ import { promisify as promisify46 } from "node:util";
142949
144445
  function quoteGitArg(value) {
142950
144446
  return `'${value.replaceAll("'", "'\\''")}'`;
142951
144447
  }
@@ -143203,7 +144699,7 @@ var init_github = __esm({
143203
144699
  "../dashboard/src/github.ts"() {
143204
144700
  "use strict";
143205
144701
  init_src();
143206
- execAsync33 = promisify45(exec37);
144702
+ execAsync33 = promisify46(exec37);
143207
144703
  PR_REVIEW_PAGE_SIZE = 100;
143208
144704
  MAX_PR_REVIEW_PAGES = 10;
143209
144705
  MAX_BADGE_BATCH_SIZE = 100;
@@ -143871,8 +145367,8 @@ var init_github = __esm({
143871
145367
  { timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
143872
145368
  );
143873
145369
  const mergeBase = mergeBaseStdout.trim();
143874
- const indexDir = await mkdtemp2(join53(tmpdir7(), "fn-pr-conflict-"));
143875
- const indexPath = join53(indexDir, "index");
145370
+ const indexDir = await mkdtemp3(join54(tmpdir8(), "fn-pr-conflict-"));
145371
+ const indexPath = join54(indexDir, "index");
143876
145372
  const gitEnv = { ...process.env, GIT_INDEX_FILE: indexPath };
143877
145373
  try {
143878
145374
  await execAsync33(`git -C ${quoteGitArg(opts.repoRoot)} read-tree -m ${quoteGitArg(mergeBase)} ${quoteGitArg(baseRef)} ${quoteGitArg(headRef)}`, {
@@ -143891,7 +145387,7 @@ var init_github = __esm({
143891
145387
  )
143892
145388
  ];
143893
145389
  } finally {
143894
- await rm6(indexDir, { recursive: true, force: true });
145390
+ await rm7(indexDir, { recursive: true, force: true });
143895
145391
  }
143896
145392
  } catch {
143897
145393
  conflictingFiles = [];
@@ -145923,8 +147419,18 @@ var init_github_tracking_reconciler = __esm({
145923
147419
  }
145924
147420
  });
145925
147421
 
147422
+ // ../dashboard/src/github-source-issue-close.ts
147423
+ var init_github_source_issue_close = __esm({
147424
+ "../dashboard/src/github-source-issue-close.ts"() {
147425
+ "use strict";
147426
+ init_github_auth();
147427
+ init_github();
147428
+ init_github_tracking_state();
147429
+ }
147430
+ });
147431
+
145926
147432
  // ../dashboard/src/github-poll.ts
145927
- import { EventEmitter as EventEmitter30 } from "node:events";
147433
+ import { EventEmitter as EventEmitter31 } from "node:events";
145928
147434
  function toAlias(type, number) {
145929
147435
  return `${type}_${number}`;
145930
147436
  }
@@ -145970,7 +147476,7 @@ var init_github_poll = __esm({
145970
147476
  }
145971
147477
  };
145972
147478
  githubRateLimiter = new GitHubRateLimiter();
145973
- GitHubPollingService = class extends EventEmitter30 {
147479
+ GitHubPollingService = class extends EventEmitter31 {
145974
147480
  watches = /* @__PURE__ */ new Map();
145975
147481
  rateLimiter;
145976
147482
  pollingIntervalMs;
@@ -146215,10 +147721,10 @@ var init_github_poll = __esm({
146215
147721
  });
146216
147722
 
146217
147723
  // ../dashboard/src/pr-metadata-generator.ts
146218
- import { promisify as promisify46 } from "node:util";
147724
+ import { promisify as promisify47 } from "node:util";
146219
147725
  import { exec as execCb } from "node:child_process";
146220
147726
  import { access as access8, readFile as readFile26 } from "node:fs/promises";
146221
- import { join as join54 } from "node:path";
147727
+ import { join as join55 } from "node:path";
146222
147728
  function buildFallback(task) {
146223
147729
  return {
146224
147730
  title: task.title ?? task.id,
@@ -146351,12 +147857,12 @@ async function generatePrMetadata(input) {
146351
147857
  ]);
146352
147858
  let promptContent = "";
146353
147859
  try {
146354
- const promptPath = join54(repoRoot, ".fusion", "tasks", task.id, "PROMPT.md");
147860
+ const promptPath = join55(repoRoot, ".fusion", "tasks", task.id, "PROMPT.md");
146355
147861
  promptContent = (await readFile26(promptPath, "utf8")).trim();
146356
147862
  } catch {
146357
147863
  promptContent = "";
146358
147864
  }
146359
- const templatePath = join54(repoRoot, ".github", "pull_request_template.md");
147865
+ const templatePath = join55(repoRoot, ".github", "pull_request_template.md");
146360
147866
  const templateExists = await access8(templatePath).then(() => true).catch(() => false);
146361
147867
  const template = templateExists ? await readFile26(templatePath, "utf8") : "";
146362
147868
  const model = resolveTaskPlanningModel(task, settings);
@@ -146415,24 +147921,24 @@ var init_pr_metadata_generator = __esm({
146415
147921
  "use strict";
146416
147922
  init_src();
146417
147923
  init_src2();
146418
- execAsync34 = promisify46(execCb);
147924
+ execAsync34 = promisify47(execCb);
146419
147925
  }
146420
147926
  });
146421
147927
 
146422
147928
  // ../dashboard/src/routes/resolve-diff-base.ts
146423
- import { execFile as execFile16 } from "node:child_process";
146424
- import { promisify as promisify47 } from "node:util";
146425
- var execFileAsync12;
147929
+ import { execFile as execFile18 } from "node:child_process";
147930
+ import { promisify as promisify48 } from "node:util";
147931
+ var execFileAsync13;
146426
147932
  var init_resolve_diff_base = __esm({
146427
147933
  "../dashboard/src/routes/resolve-diff-base.ts"() {
146428
147934
  "use strict";
146429
- execFileAsync12 = promisify47(execFile16);
147935
+ execFileAsync13 = promisify48(execFile18);
146430
147936
  }
146431
147937
  });
146432
147938
 
146433
147939
  // ../dashboard/src/routes/register-git-github.ts
146434
147940
  import { exec as execCb2, spawn as spawn12 } from "node:child_process";
146435
- import { promisify as promisify48 } from "node:util";
147941
+ import { promisify as promisify49 } from "node:util";
146436
147942
  var execAsync35, PR_ROUTE_MAX_BUFFER_BYTES;
146437
147943
  var init_register_git_github = __esm({
146438
147944
  "../dashboard/src/routes/register-git-github.ts"() {
@@ -146445,12 +147951,13 @@ var init_register_git_github = __esm({
146445
147951
  init_github_tracking_comments();
146446
147952
  init_github_tracking_state();
146447
147953
  init_github_tracking_reconciler();
147954
+ init_github_source_issue_close();
146448
147955
  init_github_poll();
146449
147956
  init_project_store_resolver();
146450
147957
  init_pr_metadata_generator();
146451
147958
  init_github_webhooks();
146452
147959
  init_resolve_diff_base();
146453
- execAsync35 = promisify48(execCb2);
147960
+ execAsync35 = promisify49(execCb2);
146454
147961
  PR_ROUTE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
146455
147962
  }
146456
147963
  });
@@ -146489,8 +147996,8 @@ var init_register_planning_subtask_routes = __esm({
146489
147996
  });
146490
147997
 
146491
147998
  // ../dashboard/src/chat.ts
146492
- import { EventEmitter as EventEmitter31 } from "node:events";
146493
- import { SessionManager as SessionManager3 } from "@mariozechner/pi-coding-agent";
147999
+ import { EventEmitter as EventEmitter32 } from "node:events";
148000
+ import { SessionManager as SessionManager3 } from "@earendil-works/pi-coding-agent";
146494
148001
  var defaultDiagnostics, _diagnostics, diagnostics6, RATE_LIMIT_WINDOW_MS5, MAX_REFERENCED_FILE_SIZE, ChatStreamManager, chatStreamManager;
146495
148002
  var init_chat = __esm({
146496
148003
  "../dashboard/src/chat.ts"() {
@@ -146524,7 +148031,7 @@ var init_chat = __esm({
146524
148031
  };
146525
148032
  RATE_LIMIT_WINDOW_MS5 = 60 * 1e3;
146526
148033
  MAX_REFERENCED_FILE_SIZE = 50 * 1024;
146527
- ChatStreamManager = class extends EventEmitter31 {
148034
+ ChatStreamManager = class extends EventEmitter32 {
146528
148035
  constructor(bufferSize = 100) {
146529
148036
  super();
146530
148037
  this.bufferSize = bufferSize;
@@ -151308,7 +152815,7 @@ var init_register_messaging_scripts = __esm({
151308
152815
  // ../dashboard/src/terminal.ts
151309
152816
  import { spawn as spawn13 } from "node:child_process";
151310
152817
  import { randomUUID as randomUUID25 } from "node:crypto";
151311
- import { EventEmitter as EventEmitter32 } from "node:events";
152818
+ import { EventEmitter as EventEmitter33 } from "node:events";
151312
152819
  function extractBaseCommand(command) {
151313
152820
  let trimmed = command.trim();
151314
152821
  while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
@@ -151454,7 +152961,7 @@ var init_terminal = __esm({
151454
152961
  // >(...) — process substitution (bash)
151455
152962
  ];
151456
152963
  CHAIN_OPERATORS = /&&|\|\||;|(?<!\|)\|(?!\|)/;
151457
- TerminalSessionManager = class extends EventEmitter32 {
152964
+ TerminalSessionManager = class extends EventEmitter33 {
151458
152965
  sessions = /* @__PURE__ */ new Map();
151459
152966
  defaultTimeout = 3e4;
151460
152967
  // 30 seconds
@@ -151662,19 +153169,19 @@ var init_register_agents_projects_nodes = __esm({
151662
153169
  });
151663
153170
 
151664
153171
  // ../dashboard/src/exec-file.ts
151665
- import { execFile as execFile17 } from "node:child_process";
151666
- import { promisify as promisify49 } from "node:util";
151667
- var execFileAsync13;
153172
+ import { execFile as execFile19 } from "node:child_process";
153173
+ import { promisify as promisify50 } from "node:util";
153174
+ var execFileAsync14;
151668
153175
  var init_exec_file = __esm({
151669
153176
  "../dashboard/src/exec-file.ts"() {
151670
153177
  "use strict";
151671
- execFileAsync13 = promisify49(execFile17);
153178
+ execFileAsync14 = promisify50(execFile19);
151672
153179
  }
151673
153180
  });
151674
153181
 
151675
153182
  // ../dashboard/src/routes/register-project-routes.ts
151676
153183
  import * as fsPromises from "node:fs/promises";
151677
- var access9, stat9, mkdir16, readdir9, rm7;
153184
+ var access9, stat9, mkdir16, readdir9, rm8;
151678
153185
  var init_register_project_routes = __esm({
151679
153186
  "../dashboard/src/routes/register-project-routes.ts"() {
151680
153187
  "use strict";
@@ -151687,7 +153194,7 @@ var init_register_project_routes = __esm({
151687
153194
  stat: stat9,
151688
153195
  mkdir: mkdir16,
151689
153196
  readdir: readdir9,
151690
- rm: rm7
153197
+ rm: rm8
151691
153198
  } = fsPromises);
151692
153199
  }
151693
153200
  });
@@ -151883,7 +153390,7 @@ var init_agent_generation = __esm({
151883
153390
 
151884
153391
  // ../dashboard/src/routes/register-agent-import-export-generation-routes.ts
151885
153392
  import * as fsPromises2 from "node:fs/promises";
151886
- var mkdtemp3, access10, stat10, mkdir17, rm8, fsWriteFile;
153393
+ var mkdtemp4, access10, stat10, mkdir17, rm9, fsWriteFile;
151887
153394
  var init_register_agent_import_export_generation_routes = __esm({
151888
153395
  "../dashboard/src/routes/register-agent-import-export-generation-routes.ts"() {
151889
153396
  "use strict";
@@ -151891,7 +153398,7 @@ var init_register_agent_import_export_generation_routes = __esm({
151891
153398
  init_ai_session_diagnostics();
151892
153399
  init_sse_buffer();
151893
153400
  init_agent_generation();
151894
- ({ mkdtemp: mkdtemp3, access: access10, stat: stat10, mkdir: mkdir17, rm: rm8, writeFile: fsWriteFile } = fsPromises2);
153401
+ ({ mkdtemp: mkdtemp4, access: access10, stat: stat10, mkdir: mkdir17, rm: rm9, writeFile: fsWriteFile } = fsPromises2);
151895
153402
  }
151896
153403
  });
151897
153404
 
@@ -153161,6 +154668,16 @@ var init_todo_routes = __esm({
153161
154668
  }
153162
154669
  });
153163
154670
 
154671
+ // ../dashboard/src/goals-routes.ts
154672
+ import { Router as Router9 } from "express";
154673
+ var init_goals_routes = __esm({
154674
+ "../dashboard/src/goals-routes.ts"() {
154675
+ "use strict";
154676
+ init_api_error();
154677
+ init_project_store_resolver();
154678
+ }
154679
+ });
154680
+
153164
154681
  // ../../plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js
153165
154682
  function compareOrderedEntities(a, b2) {
153166
154683
  if (a.orderIndex !== b2.orderIndex) {
@@ -153326,13 +154843,13 @@ var init_roadmap_ordering = __esm({
153326
154843
  });
153327
154844
 
153328
154845
  // ../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js
153329
- import { EventEmitter as EventEmitter33 } from "node:events";
154846
+ import { EventEmitter as EventEmitter34 } from "node:events";
153330
154847
  var RoadmapStore;
153331
154848
  var init_roadmap_store = __esm({
153332
154849
  "../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js"() {
153333
154850
  "use strict";
153334
154851
  init_roadmap_ordering();
153335
- RoadmapStore = class extends EventEmitter33 {
154852
+ RoadmapStore = class extends EventEmitter34 {
153336
154853
  db;
153337
154854
  /**
153338
154855
  * Creates a new RoadmapStore instance.
@@ -154836,7 +156353,7 @@ var init_src5 = __esm({
154836
156353
  });
154837
156354
 
154838
156355
  // ../dashboard/src/roadmap-routes.ts
154839
- import { Router as Router9 } from "express";
156356
+ import { Router as Router10 } from "express";
154840
156357
  var init_roadmap_routes3 = __esm({
154841
156358
  "../dashboard/src/roadmap-routes.ts"() {
154842
156359
  "use strict";
@@ -154880,7 +156397,7 @@ var init_dev_server_process = __esm({
154880
156397
  });
154881
156398
 
154882
156399
  // ../dashboard/src/dev-server-routes.ts
154883
- import { Router as Router10 } from "express";
156400
+ import { Router as Router11 } from "express";
154884
156401
  var init_dev_server_routes = __esm({
154885
156402
  "../dashboard/src/dev-server-routes.ts"() {
154886
156403
  "use strict";
@@ -154892,7 +156409,7 @@ var init_dev_server_routes = __esm({
154892
156409
  });
154893
156410
 
154894
156411
  // ../dashboard/src/routes/register-stash-recovery-routes.ts
154895
- import { Router as Router11 } from "express";
156412
+ import { Router as Router12 } from "express";
154896
156413
  var init_register_stash_recovery_routes = __esm({
154897
156414
  "../dashboard/src/routes/register-stash-recovery-routes.ts"() {
154898
156415
  "use strict";
@@ -154911,6 +156428,7 @@ var init_register_integrated_routers = __esm({
154911
156428
  init_research_routes();
154912
156429
  init_experiment_routes();
154913
156430
  init_todo_routes();
156431
+ init_goals_routes();
154914
156432
  init_roadmap_routes3();
154915
156433
  init_dev_server_routes();
154916
156434
  init_register_stash_recovery_routes();
@@ -157275,7 +158793,7 @@ var require_extension = __commonJS({
157275
158793
  var require_websocket = __commonJS({
157276
158794
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports, module) {
157277
158795
  "use strict";
157278
- var EventEmitter34 = __require("events");
158796
+ var EventEmitter35 = __require("events");
157279
158797
  var https = __require("https");
157280
158798
  var http = __require("http");
157281
158799
  var net2 = __require("net");
@@ -157307,7 +158825,7 @@ var require_websocket = __commonJS({
157307
158825
  var protocolVersions = [8, 13];
157308
158826
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
157309
158827
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
157310
- var WebSocket2 = class _WebSocket extends EventEmitter34 {
158828
+ var WebSocket2 = class _WebSocket extends EventEmitter35 {
157311
158829
  /**
157312
158830
  * Create a new `WebSocket`.
157313
158831
  *
@@ -158304,7 +159822,7 @@ var require_subprotocol = __commonJS({
158304
159822
  var require_websocket_server = __commonJS({
158305
159823
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports, module) {
158306
159824
  "use strict";
158307
- var EventEmitter34 = __require("events");
159825
+ var EventEmitter35 = __require("events");
158308
159826
  var http = __require("http");
158309
159827
  var { Duplex } = __require("stream");
158310
159828
  var { createHash: createHash15 } = __require("crypto");
@@ -158317,7 +159835,7 @@ var require_websocket_server = __commonJS({
158317
159835
  var RUNNING = 0;
158318
159836
  var CLOSING = 1;
158319
159837
  var CLOSED = 2;
158320
- var WebSocketServer2 = class extends EventEmitter34 {
159838
+ var WebSocketServer2 = class extends EventEmitter35 {
158321
159839
  /**
158322
159840
  * Create a `WebSocketServer` instance.
158323
159841
  *
@@ -158755,7 +160273,7 @@ var init_view_chunk_manifest = __esm({
158755
160273
 
158756
160274
  // ../dashboard/src/server.ts
158757
160275
  import express from "express";
158758
- import { join as join55, dirname as dirname21 } from "node:path";
160276
+ import { join as join56, dirname as dirname21 } from "node:path";
158759
160277
  import { fileURLToPath as fileURLToPath6 } from "node:url";
158760
160278
  function clearAiSessionCleanupInterval() {
158761
160279
  if (!aiSessionCleanupIntervalHandle) {
@@ -158864,6 +160382,7 @@ var init_src6 = __esm({
158864
160382
  init_rate_limit();
158865
160383
  init_github_poll();
158866
160384
  init_github_issue_comment();
160385
+ init_github_source_issue_close();
158867
160386
  init_github_tracking_comments();
158868
160387
  init_github_tracking_state();
158869
160388
  init_github_tracking_reconciler();
@@ -159060,7 +160579,7 @@ __export(task_exports, {
159060
160579
  });
159061
160580
  import { createInterface as createInterface2 } from "node:readline/promises";
159062
160581
  import { watchFile, unwatchFile, statSync as statSync7, existsSync as existsSync44, readFileSync as readFileSync15 } from "node:fs";
159063
- import { basename as basename16, join as join56 } from "node:path";
160582
+ import { basename as basename16, join as join57 } from "node:path";
159064
160583
  function formatGhErrorForCli(err) {
159065
160584
  const structured = classifyGhError(err);
159066
160585
  const lines = [`GitHub error: ${structured.message}`];
@@ -159521,7 +161040,7 @@ async function runTaskLogs(id, options = {}, projectName) {
159521
161040
  printEntries(filteredEntries);
159522
161041
  if (options.follow) {
159523
161042
  const projectPath = projectContext?.projectPath ?? process.cwd();
159524
- const logPath = join56(projectPath, ".fusion", "tasks", id, "agent.log");
161043
+ const logPath = join57(projectPath, ".fusion", "tasks", id, "agent.log");
159525
161044
  if (!existsSync44(logPath)) {
159526
161045
  console.log(`
159527
161046
  Waiting for log file to be created...`);
@@ -160699,8 +162218,8 @@ init_gh_cli();
160699
162218
  init_src2();
160700
162219
  init_src6();
160701
162220
  import { Type as Type8 } from "typebox";
160702
- import { StringEnum } from "@mariozechner/pi-ai";
160703
- import { resolve as resolve31, basename as basename17, extname as extname3, join as join57 } from "node:path";
162221
+ import { StringEnum } from "@earendil-works/pi-ai";
162222
+ import { resolve as resolve31, basename as basename17, extname as extname3, join as join58 } from "node:path";
160704
162223
  import { readFile as readFile28 } from "node:fs/promises";
160705
162224
  import { existsSync as existsSync45 } from "node:fs";
160706
162225
  import { spawn as spawn16 } from "node:child_process";
@@ -160726,12 +162245,12 @@ function resolveProjectRoot2(cwd) {
160726
162245
  warnedMissingProjectRootResolver = true;
160727
162246
  console.warn("[fusion-extension] @fusion/core.getProjectRootFromWorktree is unavailable; using filesystem fallback resolution.");
160728
162247
  }
160729
- if (worktreeProjectRoot && existsSync45(join57(worktreeProjectRoot, ".fusion"))) {
162248
+ if (worktreeProjectRoot && existsSync45(join58(worktreeProjectRoot, ".fusion"))) {
160730
162249
  return worktreeProjectRoot;
160731
162250
  }
160732
162251
  let current = resolve31(cwd);
160733
162252
  while (true) {
160734
- if (existsSync45(join57(current, ".fusion"))) {
162253
+ if (existsSync45(join58(current, ".fusion"))) {
160735
162254
  return current;
160736
162255
  }
160737
162256
  const parent = resolve31(current, "..");
@@ -160752,7 +162271,7 @@ async function getStore2(cwd) {
160752
162271
  return store;
160753
162272
  }
160754
162273
  function getFusionDir(cwd) {
160755
- return join57(resolveProjectRoot2(cwd), ".fusion");
162274
+ return join58(resolveProjectRoot2(cwd), ".fusion");
160756
162275
  }
160757
162276
  function emitSecretAudit(store, ctx, mutationType, target, metadata) {
160758
162277
  if (!ctx.runId || !ctx.agentId) return;
@@ -162616,6 +164135,136 @@ Status: ${createdMission.status}${createdMission.autoAdvance ? "\nAuto-advance:
162616
164135
  };
162617
164136
  }
162618
164137
  });
164138
+ pi2.registerTool({
164139
+ name: "fn_goal_list",
164140
+ label: "fn: List Goals",
164141
+ description: "List goals by status with active-goal warning details.",
164142
+ promptSnippet: "List project goals",
164143
+ promptGuidelines: [
164144
+ "Use to inspect current goals before creating or archiving",
164145
+ "Default status is active; pass archived/all when needed",
164146
+ "Soft warning begins at 3 active goals and hard cap is 5"
164147
+ ],
164148
+ parameters: Type8.Object({
164149
+ status: Type8.Optional(
164150
+ Type8.Union([
164151
+ Type8.Literal("active"),
164152
+ Type8.Literal("archived"),
164153
+ Type8.Literal("all")
164154
+ ], { description: "Filter by goal status (default: active)" })
164155
+ )
164156
+ }),
164157
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164158
+ const store = await getStore2(ctx.cwd);
164159
+ const goalStore = store.getGoalStore();
164160
+ const status = params.status ?? "active";
164161
+ const goals = status === "all" ? goalStore.listGoals() : goalStore.listGoals({ status });
164162
+ const activeCount = goalStore.listGoals({ status: "active" }).length;
164163
+ const softWarning = activeCount >= 3;
164164
+ const lines = [];
164165
+ lines.push(`Goals (${goals.length}) [filter: ${status}]`);
164166
+ lines.push(`Active: ${activeCount}/5`);
164167
+ if (softWarning) {
164168
+ lines.push("\u26A0 3/5 active goals \u2014 soft warning at 3, hard cap at 5");
164169
+ }
164170
+ lines.push("");
164171
+ if (goals.length === 0) {
164172
+ lines.push("No goals found.");
164173
+ } else {
164174
+ for (const goal of goals) {
164175
+ const description = goal.description ? ` \u2014 ${goal.description}` : "";
164176
+ lines.push(`- ${goal.id} [${goal.status}] ${goal.title}${description}`);
164177
+ }
164178
+ }
164179
+ return {
164180
+ content: [{ type: "text", text: lines.join("\n") }],
164181
+ details: { goals, activeCount, softWarning, hardLimit: 5 }
164182
+ };
164183
+ }
164184
+ });
164185
+ pi2.registerTool({
164186
+ name: "fn_goal_create",
164187
+ label: "fn: Create Goal",
164188
+ description: "Create a new project goal.",
164189
+ promptSnippet: "Create a new goal",
164190
+ promptGuidelines: [
164191
+ "Use clear titles and optional context-rich descriptions",
164192
+ "Goal creation counts toward the 5 active-goal hard cap",
164193
+ "Archive older goals when the active cap is reached"
164194
+ ],
164195
+ parameters: Type8.Object({
164196
+ title: Type8.String({ description: "Goal title \u2014 brief but descriptive" }),
164197
+ description: Type8.Optional(
164198
+ Type8.String({ description: "Long-form goal description (free-text markdown)" })
164199
+ )
164200
+ }),
164201
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164202
+ const store = await getStore2(ctx.cwd);
164203
+ const goalStore = store.getGoalStore();
164204
+ try {
164205
+ const goal = goalStore.createGoal({
164206
+ title: params.title.trim(),
164207
+ description: params.description?.trim() || void 0
164208
+ });
164209
+ const activeCount = goalStore.listGoals({ status: "active" }).length;
164210
+ const softWarning = activeCount >= 3;
164211
+ return {
164212
+ content: [{ type: "text", text: `Created ${goal.id}: ${goal.title}
164213
+ Status: ${goal.status}${softWarning ? `
164214
+ \u26A0 ${activeCount}/5 active goals \u2014 approaching hard cap` : ""}` }],
164215
+ details: { goalId: goal.id, title: goal.title, status: goal.status, softWarning }
164216
+ };
164217
+ } catch (err) {
164218
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ACTIVE_GOAL_LIMIT_EXCEEDED") {
164219
+ const limit = err.limit ?? 5;
164220
+ const currentActive = err.currentActive ?? 5;
164221
+ return {
164222
+ isError: true,
164223
+ content: [{ type: "text", text: `Cannot create goal \u2014 already at the hard cap of 5 active goals (currently ${currentActive}). Archive one first.` }],
164224
+ details: { code: "ACTIVE_GOAL_LIMIT_EXCEEDED", limit, currentActive }
164225
+ };
164226
+ }
164227
+ throw err;
164228
+ }
164229
+ }
164230
+ });
164231
+ pi2.registerTool({
164232
+ name: "fn_goal_archive",
164233
+ label: "fn: Archive Goal",
164234
+ description: "Archive a goal by ID.",
164235
+ promptSnippet: "Archive a goal by ID",
164236
+ promptGuidelines: [
164237
+ "Use when a goal is complete or no longer active",
164238
+ "Archiving frees active-goal capacity",
164239
+ "Returns success for already archived goals"
164240
+ ],
164241
+ parameters: Type8.Object({
164242
+ id: Type8.String({ description: "Goal ID (G-\u2026) to archive" })
164243
+ }),
164244
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
164245
+ const store = await getStore2(ctx.cwd);
164246
+ const goalStore = store.getGoalStore();
164247
+ const goal = goalStore.getGoal(params.id);
164248
+ if (!goal) {
164249
+ return {
164250
+ isError: true,
164251
+ content: [{ type: "text", text: `Goal ${params.id} not found` }],
164252
+ details: { code: "GOAL_NOT_FOUND", goalId: params.id }
164253
+ };
164254
+ }
164255
+ if (goal.status === "archived") {
164256
+ return {
164257
+ content: [{ type: "text", text: `Goal ${params.id} is already archived` }],
164258
+ details: { goalId: params.id, status: "archived" }
164259
+ };
164260
+ }
164261
+ const archived = goalStore.archiveGoal(params.id);
164262
+ return {
164263
+ content: [{ type: "text", text: `Archived ${archived.id}: ${archived.title}` }],
164264
+ details: { goalId: archived.id, status: "archived" }
164265
+ };
164266
+ }
164267
+ });
162619
164268
  pi2.registerTool({
162620
164269
  name: "fn_mission_show",
162621
164270
  label: "fn: Show Mission",