@runfusion/fusion 0.35.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 (63) hide show
  1. package/dist/bin.js +2526 -575
  2. package/dist/client/.vite/manifest.json +59 -59
  3. package/dist/client/assets/{AgentDetailView-CFX3jxfA.js → AgentDetailView-CGycokdR.js} +1 -1
  4. package/dist/client/assets/{AgentPermissionPolicyEditor-4tnYHS0-.js → AgentPermissionPolicyEditor-BoD6come.js} +1 -1
  5. package/dist/client/assets/{AgentsView-DnNWfwiY.js → AgentsView-B_afRRfc.js} +3 -3
  6. package/dist/client/assets/{ChatView-rwl2O-s0.js → ChatView-IjNrSZSf.js} +1 -1
  7. package/dist/client/assets/{DevServerView-C9fEzcFQ.js → DevServerView-Dd613aZ5.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-Cwyr0Sl4.js → DirectoryPicker-H-fUl2dF.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-gDWTMU2w.js → DocumentsView-Dshc9e5K.js} +1 -1
  10. package/dist/client/assets/{EvalsView-Cw7D1x0W.js → EvalsView-50W0K5h7.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-8yeOcqbX.js → ExperimentalAgentOnboardingModal-BXBljPFj.js} +1 -1
  12. package/dist/client/assets/{GoalsView-kRIhgAA6.js → GoalsView-Dh508Z2s.js} +1 -1
  13. package/dist/client/assets/{InsightsView-IHHikd5n.js → InsightsView-sVbGRKTl.js} +1 -1
  14. package/dist/client/assets/{MemoryView-pii3HOba.js → MemoryView-006a_sB0.js} +1 -1
  15. package/dist/client/assets/{NodesView-CoDK14B3.js → NodesView-CiRPqgVx.js} +1 -1
  16. package/dist/client/assets/{PiExtensionsManager-vH7EJ5JD.js → PiExtensionsManager-Y1ETuTxf.js} +1 -1
  17. package/dist/client/assets/{PluginManager-PbkFQ0Nu.js → PluginManager-Df_KHiXn.js} +1 -1
  18. package/dist/client/assets/{ResearchView-Dp4g_BWq.js → ResearchView-DU_QuS_-.js} +1 -1
  19. package/dist/client/assets/{SecretsView-Q8wES8_N.js → SecretsView-D61Jvysn.js} +1 -1
  20. package/dist/client/assets/{SettingsModal-DdUztATB.js → SettingsModal-BarzmqNb.js} +1 -1
  21. package/dist/client/assets/{SettingsModal-127YAB8c.js → SettingsModal-Cpur3WpT.js} +4 -4
  22. package/dist/client/assets/{SetupWizardModal-BSAFiQpJ.js → SetupWizardModal-BF3iRNq9.js} +1 -1
  23. package/dist/client/assets/{SkillsView-Bj8KQQXB.js → SkillsView-tCKOFw3j.js} +1 -1
  24. package/dist/client/assets/{StashRecoveryView-DvKN_b_k.js → StashRecoveryView-nGtypd10.js} +1 -1
  25. package/dist/client/assets/{TodoView-C-cabt2D.js → TodoView-IyGPV6AO.js} +1 -1
  26. package/dist/client/assets/{dashboard-view-DrHb8VOl.js → dashboard-view-DiA5-hnb.js} +1 -1
  27. package/dist/client/assets/{folder-open-CYVKFKAx.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-eQh-mH3-.js → upload-D6_BjqVi.js} +1 -1
  31. package/dist/client/assets/{users-B5upejXg.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 +1738 -277
  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/index-Cuh_Pqv0.css +0 -1
  63. package/dist/client/assets/index-Cze5qbME.js +0 -742
package/dist/extension.js CHANGED
@@ -328,6 +328,7 @@ var init_settings_schema = __esm({
328
328
  ],
329
329
  prerebaseDivergenceThreshold: 50,
330
330
  mergeConflictStrategy: "smart-prefer-main",
331
+ merger: { mode: "ai", maxReviewPasses: 3 },
331
332
  mergeDiffVolumeMinLines: void 0,
332
333
  mergeDiffVolumeThreshold: void 0,
333
334
  mergeDiffVolumeAllowlist: void 0,
@@ -407,6 +408,7 @@ var init_settings_schema = __esm({
407
408
  autoUpdatePrStatus: false,
408
409
  githubCommentOnDone: false,
409
410
  githubCommentTemplate: void 0,
411
+ githubCloseSourceIssueOnDone: false,
410
412
  githubTrackingEnabledByDefault: false,
411
413
  githubTrackingDefaultRepo: void 0,
412
414
  githubTrackingDedupEnabled: true,
@@ -1271,6 +1273,9 @@ function normalizePostMergeAuditMode(value) {
1271
1273
  function normalizeMergeAuditAutoRecovery(value) {
1272
1274
  return typeof value === "string" && MERGE_AUDIT_AUTO_RECOVERY_MODES.includes(value) ? value : "ai-assisted";
1273
1275
  }
1276
+ function normalizeMergerMode(value) {
1277
+ return typeof value === "string" && MERGER_MODES.includes(value) ? value : "ai";
1278
+ }
1274
1279
  function normalizeAutoRecovery(value) {
1275
1280
  const fallback = { mode: "deterministic-only", maxRetries: 3 };
1276
1281
  if (!value || typeof value !== "object") return fallback;
@@ -1577,7 +1582,7 @@ function validateMessageMetadata(metadata) {
1577
1582
  throw new Error("metadata.wakeRecipient must be a boolean");
1578
1583
  }
1579
1584
  }
1580
- var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, warnedLegacyCwdMain, MERGE_ADVANCE_AUTO_SYNC_MODES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, DUPLICATE_OF_METADATA_KEY, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, SECRET_ACCESS_POLICIES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
1585
+ var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, warnedLegacyCwdMain, MERGE_ADVANCE_AUTO_SYNC_MODES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, MERGER_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, DUPLICATE_OF_METADATA_KEY, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, SECRET_ACCESS_POLICIES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
1581
1586
  var init_types = __esm({
1582
1587
  "../core/src/types.ts"() {
1583
1588
  "use strict";
@@ -1662,6 +1667,7 @@ var init_types = __esm({
1662
1667
  ];
1663
1668
  POST_MERGE_AUDIT_MODES = ["block", "warn", "off"];
1664
1669
  MERGE_AUDIT_AUTO_RECOVERY_MODES = ["deterministic-only", "programmatic", "ai-assisted", "off"];
1670
+ MERGER_MODES = ["ai", "deterministic"];
1665
1671
  AUTO_RECOVERY_MODES = ["off", "deterministic-only", "programmatic", "ai-assisted"];
1666
1672
  NOTIFICATION_EVENTS = [
1667
1673
  "in-review",
@@ -4666,7 +4672,7 @@ var init_db = __esm({
4666
4672
  DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
4667
4673
  DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
4668
4674
  DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
4669
- SCHEMA_VERSION = 91;
4675
+ SCHEMA_VERSION = 92;
4670
4676
  SCHEMA_SQL = `
4671
4677
  -- Tasks table with JSON columns for nested data
4672
4678
  CREATE TABLE IF NOT EXISTS tasks (
@@ -5219,6 +5225,17 @@ CREATE TABLE IF NOT EXISTS missions (
5219
5225
  updatedAt TEXT NOT NULL
5220
5226
  );
5221
5227
 
5228
+ -- Goals table (strategic intent across mission timelines)
5229
+ CREATE TABLE IF NOT EXISTS goals (
5230
+ id TEXT PRIMARY KEY,
5231
+ title TEXT NOT NULL,
5232
+ description TEXT,
5233
+ status TEXT NOT NULL,
5234
+ createdAt TEXT NOT NULL,
5235
+ updatedAt TEXT NOT NULL
5236
+ );
5237
+ CREATE INDEX IF NOT EXISTS idxGoalsStatus ON goals(status);
5238
+
5222
5239
  -- Milestones table (phases within a mission)
5223
5240
  CREATE TABLE IF NOT EXISTS milestones (
5224
5241
  id TEXT PRIMARY KEY,
@@ -7531,6 +7548,24 @@ This means a caller passed a .fusion directory where a project root was expected
7531
7548
  this.addColumnIfMissing("missions", "baseBranch", "TEXT");
7532
7549
  });
7533
7550
  }
7551
+ if (version < 92) {
7552
+ this.applyMigration(92, () => {
7553
+ this.db.exec(`
7554
+ CREATE TABLE IF NOT EXISTS goals (
7555
+ id TEXT PRIMARY KEY,
7556
+ title TEXT NOT NULL,
7557
+ description TEXT,
7558
+ status TEXT NOT NULL,
7559
+ createdAt TEXT NOT NULL,
7560
+ updatedAt TEXT NOT NULL
7561
+ )
7562
+ `);
7563
+ this.db.exec(`
7564
+ CREATE INDEX IF NOT EXISTS idxGoalsStatus
7565
+ ON goals(status)
7566
+ `);
7567
+ });
7568
+ }
7534
7569
  }
7535
7570
  /**
7536
7571
  * Run a single migration step inside a transaction and bump the version.
@@ -18654,6 +18689,158 @@ var init_todo_store = __esm({
18654
18689
  }
18655
18690
  });
18656
18691
 
18692
+ // ../core/src/goal-types.ts
18693
+ var ACTIVE_GOAL_LIMIT, ActiveGoalLimitExceededError;
18694
+ var init_goal_types = __esm({
18695
+ "../core/src/goal-types.ts"() {
18696
+ "use strict";
18697
+ ACTIVE_GOAL_LIMIT = 5;
18698
+ ActiveGoalLimitExceededError = class extends Error {
18699
+ constructor(limit, currentActive) {
18700
+ super(`Active goal limit exceeded: ${currentActive}/${limit}`);
18701
+ this.limit = limit;
18702
+ this.currentActive = currentActive;
18703
+ this.name = "ActiveGoalLimitExceededError";
18704
+ }
18705
+ code = "ACTIVE_GOAL_LIMIT_EXCEEDED";
18706
+ };
18707
+ }
18708
+ });
18709
+
18710
+ // ../core/src/goal-store.ts
18711
+ import { EventEmitter as EventEmitter10 } from "node:events";
18712
+ var GoalStore;
18713
+ var init_goal_store = __esm({
18714
+ "../core/src/goal-store.ts"() {
18715
+ "use strict";
18716
+ init_goal_types();
18717
+ GoalStore = class extends EventEmitter10 {
18718
+ constructor(_fusionDir, db) {
18719
+ super();
18720
+ this.db = db;
18721
+ }
18722
+ idSequence = 0;
18723
+ createGoal(input) {
18724
+ const now = (/* @__PURE__ */ new Date()).toISOString();
18725
+ const goal = this.db.transactionImmediate(() => {
18726
+ const activeCountRow = this.db.prepare("SELECT COUNT(*) as count FROM goals WHERE status = 'active'").get();
18727
+ const currentActive = activeCountRow?.count ?? 0;
18728
+ if (currentActive >= ACTIVE_GOAL_LIMIT) {
18729
+ throw new ActiveGoalLimitExceededError(ACTIVE_GOAL_LIMIT, currentActive);
18730
+ }
18731
+ const created = {
18732
+ id: this.generateGoalId(),
18733
+ title: input.title,
18734
+ description: input.description,
18735
+ status: "active",
18736
+ createdAt: now,
18737
+ updatedAt: now
18738
+ };
18739
+ this.db.prepare(
18740
+ "INSERT INTO goals (id, title, description, status, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)"
18741
+ ).run(created.id, created.title, created.description ?? null, created.status, created.createdAt, created.updatedAt);
18742
+ return created;
18743
+ });
18744
+ this.db.bumpLastModified();
18745
+ this.emit("goal:created", goal);
18746
+ return goal;
18747
+ }
18748
+ updateGoal(id, input) {
18749
+ const existing = this.getGoal(id);
18750
+ if (!existing) {
18751
+ throw new Error(`Goal ${id} not found`);
18752
+ }
18753
+ const updated = {
18754
+ ...existing,
18755
+ title: input.title ?? existing.title,
18756
+ description: input.description ?? existing.description,
18757
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18758
+ };
18759
+ this.db.prepare("UPDATE goals SET title = ?, description = ?, updatedAt = ? WHERE id = ?").run(updated.title, updated.description ?? null, updated.updatedAt, id);
18760
+ this.db.bumpLastModified();
18761
+ this.emit("goal:updated", updated);
18762
+ return updated;
18763
+ }
18764
+ archiveGoal(id) {
18765
+ const existing = this.getGoal(id);
18766
+ if (!existing) {
18767
+ throw new Error(`Goal ${id} not found`);
18768
+ }
18769
+ if (existing.status === "archived") {
18770
+ this.emit("goal:updated", existing);
18771
+ return existing;
18772
+ }
18773
+ const updated = {
18774
+ ...existing,
18775
+ status: "archived",
18776
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18777
+ };
18778
+ this.db.prepare("UPDATE goals SET status = 'archived', updatedAt = ? WHERE id = ?").run(updated.updatedAt, id);
18779
+ this.db.bumpLastModified();
18780
+ this.emit("goal:updated", updated);
18781
+ return updated;
18782
+ }
18783
+ unarchiveGoal(id) {
18784
+ const { goal, changed } = this.db.transactionImmediate(() => {
18785
+ const existing = this.getGoal(id);
18786
+ if (!existing) {
18787
+ throw new Error(`Goal ${id} not found`);
18788
+ }
18789
+ if (existing.status === "active") {
18790
+ return { goal: existing, changed: false };
18791
+ }
18792
+ const activeCountRow = this.db.prepare("SELECT COUNT(*) as count FROM goals WHERE status = 'active'").get();
18793
+ const currentActive = activeCountRow?.count ?? 0;
18794
+ if (currentActive >= ACTIVE_GOAL_LIMIT) {
18795
+ throw new ActiveGoalLimitExceededError(ACTIVE_GOAL_LIMIT, currentActive);
18796
+ }
18797
+ const updated = {
18798
+ ...existing,
18799
+ status: "active",
18800
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18801
+ };
18802
+ this.db.prepare("UPDATE goals SET status = 'active', updatedAt = ? WHERE id = ?").run(updated.updatedAt, id);
18803
+ return { goal: updated, changed: true };
18804
+ });
18805
+ if (!changed) {
18806
+ return goal;
18807
+ }
18808
+ this.db.bumpLastModified();
18809
+ this.emit("goal:updated", goal);
18810
+ return goal;
18811
+ }
18812
+ listGoals(filter) {
18813
+ const rows = filter?.status ? this.db.prepare("SELECT id, title, description, status, createdAt, updatedAt FROM goals WHERE status = ? ORDER BY createdAt ASC").all(filter.status) : this.db.prepare("SELECT id, title, description, status, createdAt, updatedAt FROM goals ORDER BY createdAt ASC").all();
18814
+ return rows.map((row) => this.toGoal(row));
18815
+ }
18816
+ getGoal(id) {
18817
+ const row = this.db.prepare("SELECT id, title, description, status, createdAt, updatedAt FROM goals WHERE id = ?").get(id);
18818
+ if (!row) {
18819
+ return null;
18820
+ }
18821
+ return this.toGoal(row);
18822
+ }
18823
+ toGoal(row) {
18824
+ return {
18825
+ id: row.id,
18826
+ title: row.title,
18827
+ description: row.description ?? void 0,
18828
+ status: row.status,
18829
+ createdAt: row.createdAt,
18830
+ updatedAt: row.updatedAt
18831
+ };
18832
+ }
18833
+ generateGoalId() {
18834
+ const timestamp = Date.now().toString(36).toUpperCase();
18835
+ this.idSequence += 1;
18836
+ const sequence = this.idSequence.toString(36).toUpperCase().padStart(4, "0");
18837
+ const random = Math.random().toString(36).substring(2, 6).toUpperCase();
18838
+ return `G-${timestamp}-${sequence}-${random}`;
18839
+ }
18840
+ };
18841
+ }
18842
+ });
18843
+
18657
18844
  // ../core/src/eval-types.ts
18658
18845
  function normalizeEvalFollowUpText(value) {
18659
18846
  return value.toLowerCase().replace(/\s+/g, " ").replace(/[^a-z0-9 ]/g, "").trim();
@@ -18732,7 +18919,7 @@ var init_eval_types = __esm({
18732
18919
  });
18733
18920
 
18734
18921
  // ../core/src/eval-store.ts
18735
- import { EventEmitter as EventEmitter10 } from "node:events";
18922
+ import { EventEmitter as EventEmitter11 } from "node:events";
18736
18923
  import { randomUUID as randomUUID9 } from "node:crypto";
18737
18924
  function generateRunId3() {
18738
18925
  return `ER-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`;
@@ -18828,7 +19015,7 @@ var init_eval_store = __esm({
18828
19015
  this.name = "EvalLifecycleError";
18829
19016
  }
18830
19017
  };
18831
- EvalStore = class extends EventEmitter10 {
19018
+ EvalStore = class extends EventEmitter11 {
18832
19019
  constructor(db) {
18833
19020
  super();
18834
19021
  this.db = db;
@@ -19788,7 +19975,7 @@ var require_registry = __commonJS({
19788
19975
  setTimeout(send, Math.random() * 250);
19789
19976
  }
19790
19977
  announce(server, service) {
19791
- var delay4 = 1e3;
19978
+ var delay5 = 1e3;
19792
19979
  var packet = service.records();
19793
19980
  server.register(packet);
19794
19981
  const broadcast = () => {
@@ -19800,9 +19987,9 @@ var require_registry = __commonJS({
19800
19987
  service.published = true;
19801
19988
  service.emit("up");
19802
19989
  }
19803
- delay4 = delay4 * REANNOUNCE_FACTOR;
19804
- if (delay4 < REANNOUNCE_MAX_MS && !service.destroyed) {
19805
- setTimeout(broadcast, delay4).unref();
19990
+ delay5 = delay5 * REANNOUNCE_FACTOR;
19991
+ if (delay5 < REANNOUNCE_MAX_MS && !service.destroyed) {
19992
+ setTimeout(broadcast, delay5).unref();
19806
19993
  }
19807
19994
  });
19808
19995
  };
@@ -22628,7 +22815,7 @@ var require_dist = __commonJS({
22628
22815
  });
22629
22816
 
22630
22817
  // ../core/src/node-discovery.ts
22631
- import { EventEmitter as EventEmitter11 } from "node:events";
22818
+ import { EventEmitter as EventEmitter12 } from "node:events";
22632
22819
  import os from "node:os";
22633
22820
  var import_bonjour_service, DEFAULT_DISCOVERY_CONFIG, STALE_CLEANUP_INTERVAL_MS, FUSION_VERSION, NodeDiscovery;
22634
22821
  var init_node_discovery = __esm({
@@ -22644,7 +22831,7 @@ var init_node_discovery = __esm({
22644
22831
  };
22645
22832
  STALE_CLEANUP_INTERVAL_MS = 6e4;
22646
22833
  FUSION_VERSION = "0.1.0";
22647
- NodeDiscovery = class extends EventEmitter11 {
22834
+ NodeDiscovery = class extends EventEmitter12 {
22648
22835
  config;
22649
22836
  bonjour = null;
22650
22837
  broadcastService = null;
@@ -23180,7 +23367,7 @@ var init_project_identity = __esm({
23180
23367
  });
23181
23368
 
23182
23369
  // ../core/src/central-core.ts
23183
- import { EventEmitter as EventEmitter12 } from "node:events";
23370
+ import { EventEmitter as EventEmitter13 } from "node:events";
23184
23371
  import { createHash as createHash5, randomUUID as randomUUID10 } from "node:crypto";
23185
23372
  import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
23186
23373
  import { mkdir as mkdir4 } from "node:fs/promises";
@@ -23198,7 +23385,7 @@ var init_central_core = __esm({
23198
23385
  init_system_metrics();
23199
23386
  init_shared_mesh_state();
23200
23387
  init_project_identity();
23201
- CentralCore = class extends EventEmitter12 {
23388
+ CentralCore = class extends EventEmitter13 {
23202
23389
  db = null;
23203
23390
  globalDir;
23204
23391
  initialized = false;
@@ -25369,10 +25556,10 @@ var init_central_core = __esm({
25369
25556
  */
25370
25557
  async generateProjectName(projectPath) {
25371
25558
  try {
25372
- const { execFile: execFile19 } = await import("node:child_process");
25373
- const { promisify: promisify50 } = await import("node:util");
25374
- const execFileAsync14 = promisify50(execFile19);
25375
- const { stdout } = await execFileAsync14(
25559
+ const { execFile: execFile20 } = await import("node:child_process");
25560
+ const { promisify: promisify51 } = await import("node:util");
25561
+ const execFileAsync15 = promisify51(execFile20);
25562
+ const { stdout } = await execFileAsync15(
25376
25563
  "git",
25377
25564
  ["remote", "get-url", "origin"],
25378
25565
  { cwd: projectPath, timeout: 5e3 }
@@ -26076,10 +26263,10 @@ var init_migration = __esm({
26076
26263
  return basename5(projectPath);
26077
26264
  }
26078
26265
  try {
26079
- const { execFile: execFile19 } = await import("node:child_process");
26080
- const { promisify: promisify50 } = await import("node:util");
26081
- const execFileAsync14 = promisify50(execFile19);
26082
- const { stdout } = await execFileAsync14(
26266
+ const { execFile: execFile20 } = await import("node:child_process");
26267
+ const { promisify: promisify51 } = await import("node:util");
26268
+ const execFileAsync15 = promisify51(execFile20);
26269
+ const { stdout } = await execFileAsync15(
26083
26270
  "git",
26084
26271
  ["remote", "get-url", "origin"],
26085
26272
  { cwd: projectPath, timeout: 1e3 }
@@ -36723,7 +36910,7 @@ var automation_store_exports = {};
36723
36910
  __export(automation_store_exports, {
36724
36911
  AutomationStore: () => AutomationStore
36725
36912
  });
36726
- import { EventEmitter as EventEmitter13 } from "node:events";
36913
+ import { EventEmitter as EventEmitter14 } from "node:events";
36727
36914
  import { join as join16 } from "node:path";
36728
36915
  import { randomUUID as randomUUID12 } from "node:crypto";
36729
36916
  var import_cron_parser, CRON_TIMEZONE, AutomationStore;
@@ -36735,7 +36922,7 @@ var init_automation_store = __esm({
36735
36922
  init_db();
36736
36923
  init_project_root_guard();
36737
36924
  CRON_TIMEZONE = "UTC";
36738
- AutomationStore = class _AutomationStore extends EventEmitter13 {
36925
+ AutomationStore = class _AutomationStore extends EventEmitter14 {
36739
36926
  constructor(rootDir, options) {
36740
36927
  super();
36741
36928
  this.rootDir = rootDir;
@@ -37827,13 +38014,13 @@ async function searchWithQmd(rootDir, options) {
37827
38014
  const command = "qmd";
37828
38015
  const limit = Math.max(1, Math.min(options.limit ?? 5, 20));
37829
38016
  try {
37830
- const { execFile: execFile19 } = await import("node:child_process");
37831
- const { promisify: promisify50 } = await import("node:util");
37832
- const execFileAsync14 = promisify50(execFile19);
37833
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync14);
38017
+ const { execFile: execFile20 } = await import("node:child_process");
38018
+ const { promisify: promisify51 } = await import("node:util");
38019
+ const execFileAsync15 = promisify51(execFile20);
38020
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync15);
37834
38021
  scheduleQmdProjectMemoryRefresh(rootDir);
37835
38022
  const args = buildQmdSearchArgs(rootDir, options);
37836
- const { stdout } = await execFileAsync14(command, args, {
38023
+ const { stdout } = await execFileAsync15(command, args, {
37837
38024
  cwd: rootDir,
37838
38025
  timeout: 4e3,
37839
38026
  maxBuffer: 1024 * 1024
@@ -37858,12 +38045,12 @@ async function searchWithQmd(rootDir, options) {
37858
38045
  return [];
37859
38046
  }
37860
38047
  }
37861
- async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync14) {
38048
+ async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync15) {
37862
38049
  const collectionName = qmdMemoryCollectionName(rootDir);
37863
38050
  const memoryDir = memoryWorkspacePath(rootDir);
37864
38051
  await mkdir6(memoryDir, { recursive: true });
37865
38052
  try {
37866
- await execFileAsync14("qmd", buildQmdCollectionAddArgs(rootDir), {
38053
+ await execFileAsync15("qmd", buildQmdCollectionAddArgs(rootDir), {
37867
38054
  cwd: rootDir,
37868
38055
  timeout: 4e3,
37869
38056
  maxBuffer: 512 * 1024
@@ -37879,9 +38066,9 @@ ${stderr}`)) {
37879
38066
  return collectionName;
37880
38067
  }
37881
38068
  async function getDefaultExecFileAsync() {
37882
- const { execFile: execFile19 } = await import("node:child_process");
37883
- const { promisify: promisify50 } = await import("node:util");
37884
- return promisify50(execFile19);
38069
+ const { execFile: execFile20 } = await import("node:child_process");
38070
+ const { promisify: promisify51 } = await import("node:util");
38071
+ return promisify51(execFile20);
37885
38072
  }
37886
38073
  async function refreshQmdProjectMemoryIndex(rootDir, options) {
37887
38074
  const key = resolve8(rootDir);
@@ -37896,14 +38083,14 @@ async function refreshQmdProjectMemoryIndex(rootDir, options) {
37896
38083
  }
37897
38084
  }
37898
38085
  const promise = (async () => {
37899
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37900
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync14);
37901
- await execFileAsync14("qmd", ["update"], {
38086
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38087
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync15);
38088
+ await execFileAsync15("qmd", ["update"], {
37902
38089
  cwd: rootDir,
37903
38090
  timeout: 3e4,
37904
38091
  maxBuffer: 1024 * 1024
37905
38092
  });
37906
- await execFileAsync14("qmd", ["embed"], {
38093
+ await execFileAsync15("qmd", ["embed"], {
37907
38094
  cwd: rootDir,
37908
38095
  timeout: 12e4,
37909
38096
  maxBuffer: 1024 * 1024
@@ -37939,12 +38126,12 @@ async function refreshQmdAgentMemoryIndex(rootDir, agentId, options) {
37939
38126
  }
37940
38127
  }
37941
38128
  const promise = (async () => {
37942
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38129
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37943
38130
  const { agentMemoryWorkspacePath: agentMemoryWorkspacePath2 } = await Promise.resolve().then(() => (init_memory_dreams(), memory_dreams_exports));
37944
38131
  const workspacePath = agentMemoryWorkspacePath2(rootDir, agentId);
37945
38132
  await mkdir6(workspacePath, { recursive: true });
37946
38133
  try {
37947
- await execFileAsync14("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
38134
+ await execFileAsync15("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
37948
38135
  cwd: rootDir,
37949
38136
  timeout: 4e3,
37950
38137
  maxBuffer: 512 * 1024
@@ -37957,8 +38144,8 @@ ${stderr}`)) {
37957
38144
  throw err;
37958
38145
  }
37959
38146
  }
37960
- await execFileAsync14("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
37961
- await execFileAsync14("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
38147
+ await execFileAsync15("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
38148
+ await execFileAsync15("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
37962
38149
  })();
37963
38150
  qmdAgentRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
37964
38151
  try {
@@ -37979,8 +38166,8 @@ function scheduleQmdAgentMemoryRefresh(rootDir, agentId) {
37979
38166
  }
37980
38167
  async function isQmdAvailable() {
37981
38168
  try {
37982
- const execFileAsync14 = await getDefaultExecFileAsync();
37983
- await execFileAsync14("qmd", ["--help"], {
38169
+ const execFileAsync15 = await getDefaultExecFileAsync();
38170
+ await execFileAsync15("qmd", ["--help"], {
37984
38171
  timeout: 3e3,
37985
38172
  maxBuffer: 128 * 1024
37986
38173
  });
@@ -37990,12 +38177,12 @@ async function isQmdAvailable() {
37990
38177
  }
37991
38178
  }
37992
38179
  async function installQmd(options) {
37993
- const execFileAsync14 = options?.execFileAsync ?? await getDefaultExecFileAsync();
38180
+ const execFileAsync15 = options?.execFileAsync ?? await getDefaultExecFileAsync();
37994
38181
  const [command, ...args] = QMD_INSTALL_COMMAND.split(" ");
37995
38182
  if (!command || args.length === 0) {
37996
38183
  throw new MemoryBackendError("BACKEND_UNAVAILABLE", "qmd install command is not configured", "qmd");
37997
38184
  }
37998
- await execFileAsync14(command, args, {
38185
+ await execFileAsync15(command, args, {
37999
38186
  timeout: 12e4,
38000
38187
  maxBuffer: 1024 * 1024
38001
38188
  });
@@ -39646,7 +39833,7 @@ var init_task_id_integrity = __esm({
39646
39833
  });
39647
39834
 
39648
39835
  // ../core/src/store.ts
39649
- import { EventEmitter as EventEmitter14 } from "node:events";
39836
+ import { EventEmitter as EventEmitter15 } from "node:events";
39650
39837
  import { randomUUID as randomUUID15 } from "node:crypto";
39651
39838
  import { mkdir as mkdir7, readdir as readdir5, readFile as readFile8, writeFile as writeFile6, rename as rename4, unlink as unlink3 } from "node:fs/promises";
39652
39839
  import { join as join19 } from "node:path";
@@ -39978,6 +40165,7 @@ var init_store = __esm({
39978
40165
  init_research_store();
39979
40166
  init_experiment_session_store();
39980
40167
  init_todo_store();
40168
+ init_goal_store();
39981
40169
  init_eval_store();
39982
40170
  init_migration();
39983
40171
  init_central_core();
@@ -40171,7 +40359,7 @@ var init_store = __esm({
40171
40359
  this.name = "HandoffInvariantViolationError";
40172
40360
  }
40173
40361
  };
40174
- TaskStore = class _TaskStore extends EventEmitter14 {
40362
+ TaskStore = class _TaskStore extends EventEmitter15 {
40175
40363
  constructor(rootDir, globalSettingsDir, options) {
40176
40364
  super();
40177
40365
  this.rootDir = rootDir;
@@ -40303,6 +40491,8 @@ var init_store = __esm({
40303
40491
  experimentSessionStore = null;
40304
40492
  /** Cached TodoStore instance */
40305
40493
  todoStore = null;
40494
+ /** Cached GoalStore instance */
40495
+ goalStore = null;
40306
40496
  /** Cached EvalStore instance */
40307
40497
  evalStore = null;
40308
40498
  /** Cached SecretsStore instance */
@@ -42686,9 +42876,9 @@ ${outcome}`;
42686
42876
  if (validation.invalid.length > 0) {
42687
42877
  if (this.isWatching) this.taskCache.delete(id);
42688
42878
  this.deleteTaskById(id);
42689
- const { rm: rm9 } = await import("node:fs/promises");
42879
+ const { rm: rm10 } = await import("node:fs/promises");
42690
42880
  if (existsSync15(dir)) {
42691
- await rm9(dir, { recursive: true, force: true });
42881
+ await rm10(dir, { recursive: true, force: true });
42692
42882
  }
42693
42883
  throw new InvalidFileScopeError(id, validation.invalid);
42694
42884
  }
@@ -42783,10 +42973,10 @@ ${outcome}`;
42783
42973
  });
42784
42974
  if (this.isWatching) this.taskCache.delete(task.id);
42785
42975
  this.deleteTaskById(task.id);
42786
- const { rm: rm9 } = await import("node:fs/promises");
42976
+ const { rm: rm10 } = await import("node:fs/promises");
42787
42977
  const taskDir = this.taskDir(task.id);
42788
42978
  if (existsSync15(taskDir)) {
42789
- await rm9(taskDir, { recursive: true, force: true });
42979
+ await rm10(taskDir, { recursive: true, force: true });
42790
42980
  }
42791
42981
  throw new TombstonedTaskResurrectionError(
42792
42982
  tombstonedMatch.id,
@@ -45803,8 +45993,8 @@ ${newTask.description}
45803
45993
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
45804
45994
  this.db.bumpLastModified();
45805
45995
  });
45806
- const { rm: rm9 } = await import("node:fs/promises");
45807
- await rm9(dir, { recursive: true, force: true });
45996
+ const { rm: rm10 } = await import("node:fs/promises");
45997
+ await rm10(dir, { recursive: true, force: true });
45808
45998
  if (this.isWatching) {
45809
45999
  this.taskCache.delete(id);
45810
46000
  }
@@ -47213,14 +47403,14 @@ ${newTask.description}
47213
47403
  if (rows.length === 0) {
47214
47404
  return;
47215
47405
  }
47216
- const { rm: rm9 } = await import("node:fs/promises");
47406
+ const { rm: rm10 } = await import("node:fs/promises");
47217
47407
  for (const row of rows) {
47218
47408
  const task = this.rowToTask(row);
47219
47409
  const archivedAt = task.columnMovedAt ?? task.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
47220
47410
  const entry = await this.taskToArchiveEntry(task, archivedAt);
47221
47411
  this.archiveDb.upsert(entry);
47222
47412
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
47223
- await rm9(this.taskDir(task.id), { recursive: true, force: true });
47413
+ await rm10(this.taskDir(task.id), { recursive: true, force: true });
47224
47414
  if (this.isWatching) {
47225
47415
  this.taskCache.delete(task.id);
47226
47416
  }
@@ -47247,8 +47437,8 @@ ${newTask.description}
47247
47437
  this.archiveDb.upsert(entry);
47248
47438
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(task.id);
47249
47439
  this.db.bumpLastModified();
47250
- const { rm: rm9 } = await import("node:fs/promises");
47251
- await rm9(dir, { recursive: true, force: true });
47440
+ const { rm: rm10 } = await import("node:fs/promises");
47441
+ await rm10(dir, { recursive: true, force: true });
47252
47442
  if (this.isWatching) {
47253
47443
  this.taskCache.delete(task.id);
47254
47444
  }
@@ -47982,6 +48172,16 @@ ${notificationsSection}`;
47982
48172
  }
47983
48173
  return this.todoStore;
47984
48174
  }
48175
+ /**
48176
+ * Get the GoalStore instance for project-scoped goals operations.
48177
+ * Lazily initializes the GoalStore on first access.
48178
+ */
48179
+ getGoalStore() {
48180
+ if (!this.goalStore) {
48181
+ this.goalStore = new GoalStore(this.fusionDir, this.db);
48182
+ }
48183
+ return this.goalStore;
48184
+ }
47985
48185
  /**
47986
48186
  * Get the EvalStore instance for eval run and task result operations.
47987
48187
  * Lazily initializes the EvalStore on first access.
@@ -49179,8 +49379,11 @@ function describeAbortReason(reason) {
49179
49379
  if (typeof reason === "string") return reason;
49180
49380
  return "aborted";
49181
49381
  }
49382
+ function shouldAutoAppendJsonFlag(args) {
49383
+ return args[0] !== "api" && !args.includes("--json");
49384
+ }
49182
49385
  function runGhJson(args, cwd) {
49183
- const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
49386
+ const jsonArgs = shouldAutoAppendJsonFlag(args) ? [...args, "--json"] : args;
49184
49387
  const output = runGh(jsonArgs, cwd);
49185
49388
  try {
49186
49389
  return JSON.parse(output);
@@ -49189,7 +49392,7 @@ function runGhJson(args, cwd) {
49189
49392
  }
49190
49393
  }
49191
49394
  async function runGhJsonAsync(args, cwdOrOptions) {
49192
- const jsonArgs = args.includes("--json") ? args : [...args, "--json"];
49395
+ const jsonArgs = shouldAutoAppendJsonFlag(args) ? [...args, "--json"] : args;
49193
49396
  const output = await runGhAsync(jsonArgs, cwdOrOptions);
49194
49397
  try {
49195
49398
  return JSON.parse(output);
@@ -49789,7 +49992,7 @@ var routine_store_exports = {};
49789
49992
  __export(routine_store_exports, {
49790
49993
  RoutineStore: () => RoutineStore
49791
49994
  });
49792
- import { EventEmitter as EventEmitter15 } from "node:events";
49995
+ import { EventEmitter as EventEmitter16 } from "node:events";
49793
49996
  import { randomUUID as randomUUID16 } from "node:crypto";
49794
49997
  var import_cron_parser2, CRON_TIMEZONE2, RoutineStore;
49795
49998
  var init_routine_store = __esm({
@@ -49800,7 +50003,7 @@ var init_routine_store = __esm({
49800
50003
  init_routine();
49801
50004
  init_project_root_guard();
49802
50005
  CRON_TIMEZONE2 = "UTC";
49803
- RoutineStore = class _RoutineStore extends EventEmitter15 {
50006
+ RoutineStore = class _RoutineStore extends EventEmitter16 {
49804
50007
  constructor(rootDir, options) {
49805
50008
  super();
49806
50009
  this.rootDir = rootDir;
@@ -50402,7 +50605,7 @@ import { basename as basename8, dirname as dirname7, extname, isAbsolute as isAb
50402
50605
  import { stat as stat3 } from "node:fs/promises";
50403
50606
  import { copyFile } from "node:fs/promises";
50404
50607
  import { pathToFileURL } from "node:url";
50405
- import { EventEmitter as EventEmitter16 } from "node:events";
50608
+ import { EventEmitter as EventEmitter17 } from "node:events";
50406
50609
  var MINIMUM_FUSION_VERSION, moduleImportVersion, PluginLoader;
50407
50610
  var init_plugin_loader = __esm({
50408
50611
  "../core/src/plugin-loader.ts"() {
@@ -50413,7 +50616,7 @@ var init_plugin_loader = __esm({
50413
50616
  init_plugin_security_scan();
50414
50617
  MINIMUM_FUSION_VERSION = "0.1.0";
50415
50618
  moduleImportVersion = 0;
50416
- PluginLoader = class extends EventEmitter16 {
50619
+ PluginLoader = class extends EventEmitter17 {
50417
50620
  constructor(options) {
50418
50621
  super();
50419
50622
  this.options = options;
@@ -55965,15 +56168,15 @@ var require_fd_slicer = __commonJS({
55965
56168
  var Writable = stream.Writable;
55966
56169
  var PassThrough = stream.PassThrough;
55967
56170
  var Pend = require_pend();
55968
- var EventEmitter34 = __require("events").EventEmitter;
56171
+ var EventEmitter35 = __require("events").EventEmitter;
55969
56172
  exports.createFromBuffer = createFromBuffer;
55970
56173
  exports.createFromFd = createFromFd;
55971
56174
  exports.BufferSlicer = BufferSlicer;
55972
56175
  exports.FdSlicer = FdSlicer;
55973
- util.inherits(FdSlicer, EventEmitter34);
56176
+ util.inherits(FdSlicer, EventEmitter35);
55974
56177
  function FdSlicer(fd, options) {
55975
56178
  options = options || {};
55976
- EventEmitter34.call(this);
56179
+ EventEmitter35.call(this);
55977
56180
  this.fd = fd;
55978
56181
  this.pend = new Pend();
55979
56182
  this.pend.max = 1;
@@ -56117,9 +56320,9 @@ var require_fd_slicer = __commonJS({
56117
56320
  this.destroyed = true;
56118
56321
  this.context.unref();
56119
56322
  };
56120
- util.inherits(BufferSlicer, EventEmitter34);
56323
+ util.inherits(BufferSlicer, EventEmitter35);
56121
56324
  function BufferSlicer(buffer, options) {
56122
- EventEmitter34.call(this);
56325
+ EventEmitter35.call(this);
56123
56326
  options = options || {};
56124
56327
  this.refCount = 0;
56125
56328
  this.buffer = buffer;
@@ -56531,7 +56734,7 @@ var require_yauzl = __commonJS({
56531
56734
  var fd_slicer = require_fd_slicer();
56532
56735
  var crc32 = require_buffer_crc32();
56533
56736
  var util = __require("util");
56534
- var EventEmitter34 = __require("events").EventEmitter;
56737
+ var EventEmitter35 = __require("events").EventEmitter;
56535
56738
  var Transform = __require("stream").Transform;
56536
56739
  var PassThrough = __require("stream").PassThrough;
56537
56740
  var Writable = __require("stream").Writable;
@@ -56663,10 +56866,10 @@ var require_yauzl = __commonJS({
56663
56866
  callback(new Error("end of central directory record signature not found"));
56664
56867
  });
56665
56868
  }
56666
- util.inherits(ZipFile, EventEmitter34);
56869
+ util.inherits(ZipFile, EventEmitter35);
56667
56870
  function ZipFile(reader, centralDirectoryOffset, fileSize, entryCount, comment, autoClose, lazyEntries, decodeStrings, validateEntrySizes, strictFileNames) {
56668
56871
  var self = this;
56669
- EventEmitter34.call(self);
56872
+ EventEmitter35.call(self);
56670
56873
  self.reader = reader;
56671
56874
  self.reader.on("error", function(err) {
56672
56875
  emitError(self, err);
@@ -57027,9 +57230,9 @@ var require_yauzl = __commonJS({
57027
57230
  }
57028
57231
  cb();
57029
57232
  };
57030
- util.inherits(RandomAccessReader, EventEmitter34);
57233
+ util.inherits(RandomAccessReader, EventEmitter35);
57031
57234
  function RandomAccessReader() {
57032
- EventEmitter34.call(this);
57235
+ EventEmitter35.call(this);
57033
57236
  this.refCount = 0;
57034
57237
  }
57035
57238
  RandomAccessReader.prototype.ref = function() {
@@ -57160,11 +57363,11 @@ var require_extract_zip = __commonJS({
57160
57363
  var { createWriteStream, promises: fs6 } = __require("fs");
57161
57364
  var getStream = require_get_stream();
57162
57365
  var path6 = __require("path");
57163
- var { promisify: promisify50 } = __require("util");
57366
+ var { promisify: promisify51 } = __require("util");
57164
57367
  var stream = __require("stream");
57165
57368
  var yauzl = require_yauzl();
57166
- var openZip = promisify50(yauzl.open);
57167
- var pipeline = promisify50(stream.pipeline);
57369
+ var openZip = promisify51(yauzl.open);
57370
+ var pipeline = promisify51(stream.pipeline);
57168
57371
  var Extractor = class {
57169
57372
  constructor(zipPath, opts) {
57170
57373
  this.zipPath = zipPath;
@@ -57246,7 +57449,7 @@ var require_extract_zip = __commonJS({
57246
57449
  await fs6.mkdir(destDir, mkdirOptions);
57247
57450
  if (isDir) return;
57248
57451
  debug("opening read stream", dest);
57249
- const readStream = await promisify50(this.zipfile.openReadStream.bind(this.zipfile))(entry);
57452
+ const readStream = await promisify51(this.zipfile.openReadStream.bind(this.zipfile))(entry);
57250
57453
  if (symlink) {
57251
57454
  const link = await getStream(readStream);
57252
57455
  debug("creating symlink", link, dest);
@@ -68267,14 +68470,14 @@ var init_agent_companies_exporter = __esm({
68267
68470
  });
68268
68471
 
68269
68472
  // ../core/src/chat-store.ts
68270
- import { EventEmitter as EventEmitter17 } from "node:events";
68473
+ import { EventEmitter as EventEmitter18 } from "node:events";
68271
68474
  import { randomUUID as randomUUID19 } from "node:crypto";
68272
68475
  var ChatStore;
68273
68476
  var init_chat_store = __esm({
68274
68477
  "../core/src/chat-store.ts"() {
68275
68478
  "use strict";
68276
68479
  init_db();
68277
- ChatStore = class extends EventEmitter17 {
68480
+ ChatStore = class extends EventEmitter18 {
68278
68481
  constructor(fusionDir, db) {
68279
68482
  super();
68280
68483
  this.fusionDir = fusionDir;
@@ -69366,6 +69569,7 @@ var init_secrets_sync = __esm({
69366
69569
  // ../core/src/index.ts
69367
69570
  var src_exports = {};
69368
69571
  __export(src_exports, {
69572
+ ACTIVE_GOAL_LIMIT: () => ACTIVE_GOAL_LIMIT,
69369
69573
  AGENT_PERMISSIONS: () => AGENT_PERMISSIONS,
69370
69574
  AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: () => AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
69371
69575
  AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES,
@@ -69379,6 +69583,7 @@ __export(src_exports, {
69379
69583
  AUTOPILOT_STATES: () => AUTOPILOT_STATES,
69380
69584
  AUTO_RECOVERY_MODES: () => AUTO_RECOVERY_MODES,
69381
69585
  AUTO_SUMMARIZE_SCHEDULE_NAME: () => AUTO_SUMMARIZE_SCHEDULE_NAME,
69586
+ ActiveGoalLimitExceededError: () => ActiveGoalLimitExceededError,
69382
69587
  AgentCompaniesParseError: () => AgentCompaniesParseError,
69383
69588
  AgentStore: () => AgentStore,
69384
69589
  AiServiceError: () => AiServiceError,
@@ -69472,6 +69677,7 @@ __export(src_exports, {
69472
69677
  FirstRunDetector: () => FirstRunDetector,
69473
69678
  GLOBAL_SETTINGS_KEYS: () => GLOBAL_SETTINGS_KEYS,
69474
69679
  GlobalSettingsStore: () => GlobalSettingsStore,
69680
+ GoalStore: () => GoalStore,
69475
69681
  HIGH_FANOUT_BLOCKER_TODO_THRESHOLD: () => HIGH_FANOUT_BLOCKER_TODO_THRESHOLD,
69476
69682
  HandoffInvariantViolationError: () => HandoffInvariantViolationError,
69477
69683
  INSIGHT_EXTRACTION_SCHEDULE_NAME: () => INSIGHT_EXTRACTION_SCHEDULE_NAME,
@@ -69506,6 +69712,7 @@ __export(src_exports, {
69506
69712
  MEMORY_LONG_TERM_FILENAME: () => MEMORY_LONG_TERM_FILENAME,
69507
69713
  MEMORY_WORKING_PATH: () => MEMORY_WORKING_PATH,
69508
69714
  MEMORY_WORKSPACE_PATH: () => MEMORY_WORKSPACE_PATH,
69715
+ MERGER_MODES: () => MERGER_MODES,
69509
69716
  MERGE_ADVANCE_AUTO_SYNC_MODES: () => MERGE_ADVANCE_AUTO_SYNC_MODES,
69510
69717
  MERGE_AUDIT_AUTO_RECOVERY_MODES: () => MERGE_AUDIT_AUTO_RECOVERY_MODES,
69511
69718
  MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT: () => MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT,
@@ -69853,6 +70060,7 @@ __export(src_exports, {
69853
70060
  normalizeMergeConflictStrategy: () => normalizeMergeConflictStrategy,
69854
70061
  normalizeMergeIntegrationWorktreeMode: () => normalizeMergeIntegrationWorktreeMode,
69855
70062
  normalizeMergeStrategyOverlapBehavior: () => normalizeMergeStrategyOverlapBehavior,
70063
+ normalizeMergerMode: () => normalizeMergerMode,
69856
70064
  normalizeMessageParticipant: () => normalizeMessageParticipant,
69857
70065
  normalizePermissions: () => normalizePermissions,
69858
70066
  normalizePluginUiContributionDefinition: () => normalizePluginUiContributionDefinition,
@@ -70110,6 +70318,8 @@ var init_src = __esm({
70110
70318
  init_task_priority();
70111
70319
  init_mission_types();
70112
70320
  init_mission_store();
70321
+ init_goal_types();
70322
+ init_goal_store();
70113
70323
  init_central_core();
70114
70324
  init_central_db();
70115
70325
  init_node_connection();
@@ -70740,15 +70950,15 @@ function evaluateAgentActionGate(params) {
70740
70950
  let resourceId;
70741
70951
  if (params.toolName === "bash") {
70742
70952
  const command = extractShellCommand(args);
70743
- const git2 = classifyGitCommand(command);
70744
- if (git2?.write) {
70953
+ const git3 = classifyGitCommand(command);
70954
+ if (git3?.write) {
70745
70955
  category = "git_write";
70746
- operation = git2.operation;
70956
+ operation = git3.operation;
70747
70957
  resourceType = "git";
70748
70958
  } else {
70749
70959
  category = "command_execution";
70750
- operation = git2?.operation ?? "shell command";
70751
- resourceType = git2 ? "git" : "command";
70960
+ operation = git3?.operation ?? "shell command";
70961
+ resourceType = git3 ? "git" : "command";
70752
70962
  }
70753
70963
  } else if (params.toolName === "write" || params.toolName === "edit") {
70754
70964
  category = "file_write_delete";
@@ -71988,8 +72198,8 @@ var init_context_limit_detector = __esm({
71988
72198
  import { existsSync as existsSync24, readFileSync as readFileSync8 } from "node:fs";
71989
72199
  import { homedir as homedir5 } from "node:os";
71990
72200
  import { join as join29 } from "node:path";
71991
- import { AuthStorage } from "@mariozechner/pi-coding-agent";
71992
- 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";
71993
72203
  function getHomeDir5() {
71994
72204
  return process.env.HOME || process.env.USERPROFILE || homedir5();
71995
72205
  }
@@ -72398,7 +72608,7 @@ import {
72398
72608
  ModelRegistry,
72399
72609
  SessionManager,
72400
72610
  SettingsManager
72401
- } from "@mariozechner/pi-coding-agent";
72611
+ } from "@earendil-works/pi-coding-agent";
72402
72612
  function normalizeRtkRewriteOptions(options) {
72403
72613
  const modeEnv = process.env.FUSION_RTK_REWRITE?.toLowerCase();
72404
72614
  const envMode = modeEnv === "1" || modeEnv === "true" || modeEnv === "rewrite" ? "rewrite" : "off";
@@ -74928,9 +75138,9 @@ var init_web_search_provider = __esm({
74928
75138
  if (!isRetryableHttp || attempt >= RETRY_MAX_ATTEMPTS) {
74929
75139
  break;
74930
75140
  }
74931
- const delay4 = Math.min(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
74932
- const jitter = delay4 * (Math.random() * 0.2 - 0.1);
74933
- const totalDelay = Math.max(0, delay4 + jitter);
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);
74934
75144
  log9.warn(`retrying ${backend} search`, { attempt, totalDelay });
74935
75145
  await sleep2(totalDelay, signal);
74936
75146
  }
@@ -75384,7 +75594,7 @@ import { appendFile as appendFile3, mkdir as mkdir12, readFile as readFile14, re
75384
75594
  import { existsSync as existsSync26 } from "node:fs";
75385
75595
  import { createHash as createHash8 } from "node:crypto";
75386
75596
  import { join as join33, relative as relative6, resolve as resolve17 } from "node:path";
75387
- import { Type } from "@mariozechner/pi-ai";
75597
+ import { Type } from "@earendil-works/pi-ai";
75388
75598
  function sanitizeAgentMemoryId(agentId) {
75389
75599
  return agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
75390
75600
  }
@@ -75554,11 +75764,11 @@ async function refreshAgentMemoryQmdIndex(rootDir, agentMemory) {
75554
75764
  return;
75555
75765
  }
75556
75766
  const promise = (async () => {
75557
- const { execFile: execFile19 } = await import("node:child_process");
75558
- const { promisify: promisify50 } = await import("node:util");
75559
- const execFileAsync14 = promisify50(execFile19);
75767
+ const { execFile: execFile20 } = await import("node:child_process");
75768
+ const { promisify: promisify51 } = await import("node:util");
75769
+ const execFileAsync15 = promisify51(execFile20);
75560
75770
  try {
75561
- await execFileAsync14("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
75771
+ await execFileAsync15("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
75562
75772
  cwd: rootDir,
75563
75773
  timeout: 4e3,
75564
75774
  maxBuffer: 512 * 1024
@@ -75571,8 +75781,8 @@ ${stderr}`)) {
75571
75781
  throw error;
75572
75782
  }
75573
75783
  }
75574
- await execFileAsync14("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
75575
- 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 });
75576
75786
  })();
75577
75787
  agentQmdRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
75578
75788
  try {
@@ -75627,10 +75837,10 @@ async function searchAgentMemoryWithQmd(rootDir, agentMemory, query, limit) {
75627
75837
  }
75628
75838
  try {
75629
75839
  await refreshAgentMemoryQmdIndex(rootDir, agentMemory);
75630
- const { execFile: execFile19 } = await import("node:child_process");
75631
- const { promisify: promisify50 } = await import("node:util");
75632
- const execFileAsync14 = promisify50(execFile19);
75633
- const { stdout } = await execFileAsync14("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
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), {
75634
75844
  cwd: rootDir,
75635
75845
  timeout: 4e3,
75636
75846
  maxBuffer: 1024 * 1024
@@ -79134,6 +79344,24 @@ var init_webhook_provider = __esm({
79134
79344
  }
79135
79345
  });
79136
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
+
79137
79365
  // ../engine/src/notification/notification-service.ts
79138
79366
  var NotificationService;
79139
79367
  var init_notification_service = __esm({
@@ -79142,6 +79370,7 @@ var init_notification_service = __esm({
79142
79370
  init_src();
79143
79371
  init_notifier();
79144
79372
  init_logger2();
79373
+ init_transient_merge_error_classifier();
79145
79374
  init_ntfy_provider();
79146
79375
  init_webhook_provider();
79147
79376
  NotificationService = class {
@@ -79243,6 +79472,14 @@ var init_notification_service = __esm({
79243
79472
  return;
79244
79473
  }
79245
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
+ }
79246
79483
  if (this.failureNotificationMode === "all") {
79247
79484
  this.maybeNotify(task.id, "failed", this.createTaskPayload(task, "failed"));
79248
79485
  } else {
@@ -79559,6 +79796,14 @@ var init_notification_service = __esm({
79559
79796
  schedulerLog.log(`[notify] ${taskId} no longer failed at dispatch time \u2014 suppressed notification`);
79560
79797
  return;
79561
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
+ }
79562
79807
  const isTerminal = task.paused === true || task.column === "in-review";
79563
79808
  if (this.failureNotificationMode === "terminal-only" && !isTerminal) {
79564
79809
  this.failureNotificationSuppressedCount += 1;
@@ -79811,6 +80056,19 @@ function truncateNtfyMessage(message) {
79811
80056
  }
79812
80057
  return best;
79813
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
+ }
79814
80072
  function resolveNtfyEvents(events) {
79815
80073
  return events ? [...events] : [...DEFAULT_NTFY_EVENTS];
79816
80074
  }
@@ -79860,10 +80118,10 @@ async function sendNtfyNotificationWithResult({
79860
80118
  const truncatedMessage = truncateNtfyMessage(message);
79861
80119
  const latin1Safe = isLatin1Safe(truncatedTitle) && isLatin1Safe(truncatedMessage);
79862
80120
  const headers = {
79863
- Priority: priority,
79864
80121
  "Content-Type": latin1Safe ? "text/plain" : "application/json"
79865
80122
  };
79866
80123
  if (latin1Safe) {
80124
+ headers.Priority = priority;
79867
80125
  headers.Title = truncatedTitle;
79868
80126
  if (clickUrl) {
79869
80127
  headers.Click = clickUrl;
@@ -79879,7 +80137,7 @@ async function sendNtfyNotificationWithResult({
79879
80137
  topic,
79880
80138
  title: truncatedTitle,
79881
80139
  message: truncatedMessage,
79882
- priority,
80140
+ priority: ntfyPriorityToInt(priority),
79883
80141
  ...clickUrl ? { click: clickUrl } : {}
79884
80142
  }),
79885
80143
  signal
@@ -85151,11 +85409,11 @@ var init_step_session_executor = __esm({
85151
85409
  }
85152
85410
  if (attempt > 0) {
85153
85411
  retries++;
85154
- const delay4 = RETRY_DELAYS_MS[attempt - 1] ?? 15e3;
85412
+ const delay5 = RETRY_DELAYS_MS[attempt - 1] ?? 15e3;
85155
85413
  stepExecLog.log(
85156
- `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}`
85157
85415
  );
85158
- await sleep3(delay4);
85416
+ await sleep3(delay5);
85159
85417
  }
85160
85418
  const agentLogger = new AgentLogger({
85161
85419
  store: this.store,
@@ -85921,9 +86179,9 @@ async function withRateLimitRetry(fn2, options = {}) {
85921
86179
  }
85922
86180
  const rawDelay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
85923
86181
  const jitter = rawDelay * 0.1 * (2 * Math.random() - 1);
85924
- const delay4 = Math.max(0, Math.round(rawDelay + jitter));
85925
- onRetry?.(attempt + 1, delay4, error);
85926
- await sleep4(delay4, signal);
86182
+ const delay5 = Math.max(0, Math.round(rawDelay + jitter));
86183
+ onRetry?.(attempt + 1, delay5, error);
86184
+ await sleep4(delay5, signal);
85927
86185
  }
85928
86186
  }
85929
86187
  throw lastError ?? new Error("withRateLimitRetry: unexpected state");
@@ -88993,10 +89251,13 @@ function decideAutoPrerebase(input) {
88993
89251
  if (hotMatches.length > 0) {
88994
89252
  return { fire: true, reason: "hot-file", commitsBehind, hotMatches };
88995
89253
  }
88996
- const threshold = input.settings.prerebaseDivergenceThreshold ?? 0;
88997
- if (threshold > 0 && commitsBehind > threshold) {
89254
+ const threshold = input.settings.prerebaseDivergenceThreshold ?? 1;
89255
+ if (threshold > 0 && commitsBehind >= threshold) {
88998
89256
  return { fire: true, reason: "divergence-threshold", commitsBehind, hotMatches: [] };
88999
89257
  }
89258
+ if (commitsBehind > 0) {
89259
+ return { fire: true, reason: "safety-fallback-any-divergence", commitsBehind, hotMatches: [] };
89260
+ }
89000
89261
  return { fire: false, reason: "no-divergence", commitsBehind, hotMatches: [] };
89001
89262
  }
89002
89263
  async function runAutoPrerebase(deps) {
@@ -90914,12 +91175,22 @@ async function advanceIntegrationBranchRef(args) {
90914
91175
  return { advanced: true, previousSha: expectedCurrentSha, newSha };
90915
91176
  } catch (error) {
90916
91177
  const diagnostic = error instanceof Error ? error.message : String(error);
90917
- const lower = diagnostic.toLowerCase();
90918
- const isConcurrent = lower.includes("cannot lock ref") || lower.includes("is at") || lower.includes("expected");
90919
- const reason = isConcurrent ? "concurrent-advance" : "ref-update-refused";
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";
90920
91191
  await emitRefAdvance({
90921
91192
  succeeded: false,
90922
- fromSha: observedCurrentSha || expectedCurrentSha,
91193
+ fromSha: effectiveObserved || expectedCurrentSha,
90923
91194
  toSha: newSha,
90924
91195
  error: `${reason}: ${diagnostic}`
90925
91196
  });
@@ -90927,7 +91198,7 @@ async function advanceIntegrationBranchRef(args) {
90927
91198
  advanced: false,
90928
91199
  reason,
90929
91200
  diagnostic,
90930
- observedCurrentSha
91201
+ observedCurrentSha: effectiveObserved
90931
91202
  };
90932
91203
  }
90933
91204
  }
@@ -91417,6 +91688,7 @@ __export(merger_exports, {
91417
91688
  classifyConflict: () => classifyConflict,
91418
91689
  classifyOwnedLandedEvidence: () => classifyOwnedLandedEvidence,
91419
91690
  commitOrAmendMergeWithFixes: () => commitOrAmendMergeWithFixes,
91691
+ composeMergeCommitBody: () => composeMergeCommitBody,
91420
91692
  deriveDeterministicSubjectSummary: () => deriveDeterministicSubjectSummary,
91421
91693
  deriveScopedPnpmTestCommand: () => deriveScopedPnpmTestCommand,
91422
91694
  detectResolvableConflicts: () => detectResolvableConflicts,
@@ -93906,6 +94178,23 @@ async function generateAiMergeSummary(commitLog, diffStat, settings, rootDir) {
93906
94178
  return null;
93907
94179
  }
93908
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
+ }
93909
94198
  async function generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, signal) {
93910
94199
  try {
93911
94200
  const resolved = resolveTitleSummarizerSettingsModel(settings);
@@ -93973,22 +94262,43 @@ function deriveDeterministicSubjectSummary(commitLog) {
93973
94262
  const summary = extras > 0 ? `${headline} (+${extras} more)` : headline;
93974
94263
  return summary;
93975
94264
  }
93976
- async function buildDeterministicMergeMessage(params) {
93977
- const { taskId, branch, commitLog, diffStat, includeTaskId, aiSummary, aiSubject } = params;
93978
- const prefix = includeTaskId ? `feat(${taskId})` : "feat";
93979
- const trimmedAiSubject = aiSubject?.trim() ?? "";
93980
- const derived = trimmedAiSubject.length === 0 ? deriveDeterministicSubjectSummary(commitLog ?? "") : null;
93981
- const subjectSummary = trimmedAiSubject.length > 0 ? trimmedAiSubject : derived ?? `merge ${branch}`;
93982
- const subject = `${prefix}: ${subjectSummary}`;
94265
+ function composeMergeCommitBody(params) {
94266
+ const { branch, commitLog, diffStat, aiSummary, aiBody } = params;
94267
+ const trimmedSummary = aiSummary?.trim() ?? "";
94268
+ const trimmedAiBody = aiBody?.trim() ?? "";
93983
94269
  const trimmedCommitLog = commitLog?.trim() ?? "";
93984
94270
  const trimmedDiffStat = diffStat?.trim() ?? "";
93985
94271
  const commitsSection = trimmedCommitLog.length > 0 ? trimmedCommitLog : `- merge ${branch}`;
93986
- 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 = [
93987
94276
  `Commits merged:
93988
94277
  ${commitsSection}`,
93989
94278
  trimmedDiffStat.length > 0 ? `Files changed:
93990
94279
  ${trimmedDiffStat}` : ""
93991
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
+ });
93992
94302
  const escape = (s3) => s3.replace(/(["\\$`])/g, "\\$1");
93993
94303
  return {
93994
94304
  subjectArg: `-m "${escape(subject)}"`,
@@ -94400,6 +94710,7 @@ ${msg}`;
94400
94710
  diffStat: messageDiffStat,
94401
94711
  includeTaskId,
94402
94712
  aiSummary,
94713
+ aiBody: void 0,
94403
94714
  aiSubject
94404
94715
  });
94405
94716
  let lineageId;
@@ -97593,8 +97904,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97593
97904
  mergerLog.log(`${taskId}: ${routeMessage}`);
97594
97905
  await store.appendAgentLog(taskId, routeMessage, "text", void 0, "merger");
97595
97906
  }
97596
- const aiMergeSummary = settings.useAiMergeCommitSummary ? await generateAiMergeSummary(commitLog, diffStat, settings, rootDir) : null;
97597
- const aiMergeSubject = settings.useAiMergeCommitSummary ? await generateAiMergeSubject(commitLog, diffStat, settings, rootDir, branch, taskId, options.signal) : null;
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];
97598
97912
  try {
97599
97913
  const scopeResult = await validateDiffScope(store, taskId, diffStat, settings.strictScopeEnforcement);
97600
97914
  for (const warning of scopeResult.warnings) {
@@ -97670,6 +97984,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
97670
97984
  commitLog,
97671
97985
  diffStat,
97672
97986
  aiSummary: aiMergeSummary,
97987
+ aiBody: aiMergeBody,
97673
97988
  aiSubject: aiMergeSubject,
97674
97989
  includeTaskId,
97675
97990
  sourceIssueRef,
@@ -98268,6 +98583,7 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98268
98583
  const recordedFilesChanged = mergeWasEmpty ? 0 : filesChanged;
98269
98584
  const recordedInsertions = mergeWasEmpty ? 0 : insertions;
98270
98585
  const recordedDeletions = mergeWasEmpty ? 0 : deletions;
98586
+ const mergeConfirmedAtThisPoint = !reuseTaskWorktreeMerge;
98271
98587
  const mergeDetails = {
98272
98588
  commitSha: recordedSha,
98273
98589
  rebaseBaseSha: !mergeWasEmpty && rebaseMergeBaseSha ? rebaseMergeBaseSha : void 0,
@@ -98278,9 +98594,11 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98278
98594
  noOpVerifiedShortCircuit,
98279
98595
  landedFilesAttributionRestricted,
98280
98596
  landedFilesCaptureFallback,
98597
+ // Keep mergeDetails headline-only for dashboard cards; rich bullet body
98598
+ // is used in the actual git commit message composition path.
98281
98599
  mergeCommitMessage: aiMergeSummary || commitLog,
98282
98600
  mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
98283
- mergeConfirmed: true,
98601
+ mergeConfirmed: mergeConfirmedAtThisPoint,
98284
98602
  mergeTargetBranch: mergeTarget.branch,
98285
98603
  mergeTargetSource: mergeTarget.source,
98286
98604
  resolutionStrategy: result.resolutionStrategy,
@@ -98374,6 +98692,19 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
98374
98692
  mergerLog.log(
98375
98693
  `${taskId}: ${integrationBranch2} advanced to ${worktreeHeadSha.slice(0, 8)} via update-ref; your checked-out worktree at ${projectRootDir} is now behind`
98376
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
+ }
98377
98708
  const autoSyncMode = normalizeMergeAdvanceAutoSyncMode(settings.mergeAdvanceAutoSync);
98378
98709
  if (autoSyncMode !== "off") {
98379
98710
  try {
@@ -98738,6 +99069,7 @@ async function executeMergeAttempt(params, aiTracker) {
98738
99069
  commitLog,
98739
99070
  diffStat,
98740
99071
  aiSummary,
99072
+ aiBody,
98741
99073
  aiSubject,
98742
99074
  includeTaskId,
98743
99075
  sourceIssueRef,
@@ -98996,6 +99328,7 @@ async function executeMergeAttempt(params, aiTracker) {
98996
99328
  commitLog,
98997
99329
  diffStat,
98998
99330
  aiSummary,
99331
+ aiBody,
98999
99332
  aiSubject,
99000
99333
  includeTaskId,
99001
99334
  hasConflicts,
@@ -99142,7 +99475,7 @@ async function attemptWithMixedSideStrategy(params, strategy, aiTracker) {
99142
99475
  }
99143
99476
  }
99144
99477
  async function finalizeSideStrategyAttempt(params, side, aiTracker) {
99145
- const { rootDir, branch, commitLog, diffStat, aiSummary, aiSubject, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
99478
+ const { rootDir, branch, commitLog, diffStat, aiSummary, aiBody, aiSubject, includeTaskId, sourceIssueRef, taskId, store, settings, testCommand, buildCommand: buildCommand2, testSource, buildSource } = params;
99146
99479
  const staged = execSyncText("git diff --cached --quiet 2>&1; echo $?", {
99147
99480
  cwd: rootDir,
99148
99481
  encoding: "utf-8"
@@ -99183,7 +99516,8 @@ async function finalizeSideStrategyAttempt(params, side, aiTracker) {
99183
99516
  commitLog,
99184
99517
  diffStat,
99185
99518
  includeTaskId,
99186
- aiSummary: aiSummary?.trim().length ? aiSummary : safeBody,
99519
+ aiSummary,
99520
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
99187
99521
  aiSubject
99188
99522
  });
99189
99523
  await enforceSquashFileScopeInvariant({
@@ -99241,6 +99575,7 @@ async function runAiAgentForCommit(params) {
99241
99575
  commitLog,
99242
99576
  diffStat,
99243
99577
  aiSummary,
99578
+ aiBody,
99244
99579
  aiSubject,
99245
99580
  includeTaskId,
99246
99581
  hasConflicts,
@@ -99462,7 +99797,8 @@ async function runAiAgentForCommit(params) {
99462
99797
  commitLog,
99463
99798
  diffStat,
99464
99799
  includeTaskId,
99465
- aiSummary: aiSummary?.trim().length ? aiSummary : safeBody,
99800
+ aiSummary,
99801
+ aiBody: aiBody?.trim().length ? aiBody : safeBody,
99466
99802
  aiSubject
99467
99803
  });
99468
99804
  await runDiffVolumeGate({
@@ -100287,7 +100623,7 @@ function isNoTaskDoneFailure2(task) {
100287
100623
  function hasStepProgress2(task) {
100288
100624
  return task.steps.some((step) => step.status !== "pending");
100289
100625
  }
100290
- var log20, worktreeMetadataReconcileLog, execAsync27, yieldEventLoop2, DONE_TASK_INTEGRITY_SWEEP_LIMIT, BOARD_STALL_NOTIFICATION_COOLDOWN_MS, DB_CORRUPTION_NOTIFICATION_COOLDOWN_MS, STALE_ACTIVE_BRANCH_EXECUTION_GRACE_MS, COMPLETION_HANDOFF_LIMBO_GRACE_MS, MAX_COMPLETION_HANDOFF_LIMBO_RECOVERIES, APPROVED_TRIAGE_RECOVERY_GRACE_MS, STARVED_REFINEMENT_RECOVERY_GRACE_MS, STARVED_PEER_PROGRESS_THRESHOLD, STARVED_REFINEMENT_ESCALATION_COOLDOWN_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES2, NON_TERMINAL_STEP_STATUSES2, STRANDED_COMPLETED_TODO_ACTIVE_STATUSES, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_WORKTREE_SESSION_RETRIES, MAX_AUTO_MERGE_RETRIES, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DEFAULT_STALE_MERGING_FANOUT_MIN_AGE_MS, DEFAULT_UNBACKED_MERGING_FANOUT_GRACE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, RUNNING_ON_INACTIVE_TASK_STALE_RUN_MS, SelfHealingManager;
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;
100291
100627
  var init_self_healing = __esm({
100292
100628
  "../engine/src/self-healing.ts"() {
100293
100629
  "use strict";
@@ -100308,6 +100644,8 @@ var init_self_healing = __esm({
100308
100644
  init_foreign_only_contamination();
100309
100645
  init_notifier();
100310
100646
  init_dependency_blocked_todo_reporter();
100647
+ init_transient_merge_error_classifier();
100648
+ init_transient_merge_error_classifier();
100311
100649
  log20 = createLogger2("self-healing");
100312
100650
  worktreeMetadataReconcileLog = createLogger2("worktree-metadata-reconcile");
100313
100651
  execAsync27 = promisify35(exec29);
@@ -100346,6 +100684,7 @@ var init_self_healing = __esm({
100346
100684
  MAX_TASK_DONE_RETRIES = 3;
100347
100685
  MAX_WORKTREE_SESSION_RETRIES = 3;
100348
100686
  MAX_AUTO_MERGE_RETRIES = 3;
100687
+ MAX_TRANSIENT_MERGE_RECOVERIES = 2;
100349
100688
  MAX_STARVATION_DROPS = 3;
100350
100689
  DEADLOCK_RECOVERY_COOLDOWN_MS = 15 * 6e4;
100351
100690
  DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS = 5 * 6e4;
@@ -100543,6 +100882,7 @@ var init_self_healing = __esm({
100543
100882
  { name: "stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks().then(() => void 0) },
100544
100883
  { name: "failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps().then(() => void 0) },
100545
100884
  { name: "interrupted-merging", fn: () => this.recoverInterruptedMergingTasks().then(() => void 0) },
100885
+ { name: "transient-merge-failures", fn: () => this.recoverTransientMergeFailures().then(() => void 0) },
100546
100886
  { name: "done-merge-metadata", fn: () => this.recoverDoneTaskMergeMetadata().then(() => void 0) },
100547
100887
  { name: "reconcile-done-task-integrity", fn: () => this.reconcileDoneTaskIntegrity().then(() => void 0) },
100548
100888
  // FN-5092: must run BEFORE any merger pickup path so the merger queue is
@@ -100637,8 +100977,8 @@ var init_self_healing = __esm({
100637
100977
  this.lastPauseTriggeredAt = Date.now();
100638
100978
  const baseDelay = settings.autoUnpauseBaseDelayMs ?? 3e5;
100639
100979
  const maxDelay = settings.autoUnpauseMaxDelayMs ?? 36e5;
100640
- const delay4 = Math.min(baseDelay * Math.pow(2, this.unpauseAttempt), maxDelay);
100641
- this.scheduleUnpause(delay4);
100980
+ const delay5 = Math.min(baseDelay * Math.pow(2, this.unpauseAttempt), maxDelay);
100981
+ this.scheduleUnpause(delay5);
100642
100982
  }
100643
100983
  if (previous.globalPause && !settings.globalPause) {
100644
100984
  this.cancelUnpauseTimer();
@@ -101072,6 +101412,7 @@ var init_self_healing = __esm({
101072
101412
  { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
101073
101413
  { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
101074
101414
  { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
101415
+ { name: "recover-transient-merge-failures", fn: () => this.recoverTransientMergeFailures() },
101075
101416
  { name: "recover-done-merge-metadata", fn: () => this.recoverDoneTaskMergeMetadata() },
101076
101417
  { name: "recover-stale-merging-status", fn: () => this.recoverStaleMergingStatus() },
101077
101418
  { name: "finalize-noop-review", fn: () => this.finalizeNoOpReviewTasks() },
@@ -104244,6 +104585,142 @@ var init_self_healing = __esm({
104244
104585
  * No-op when `settings.autoMerge === false` — PR-based review flow owns lifecycle until human merge.
104245
104586
  * @returns Number of tasks finalized or unblocked
104246
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
+ }
104247
104724
  async recoverInterruptedMergingTasks() {
104248
104725
  try {
104249
104726
  const settings = await this.store.getSettings();
@@ -106545,7 +107022,7 @@ var init_self_healing = __esm({
106545
107022
  });
106546
107023
 
106547
107024
  // ../engine/src/triage.ts
106548
- import { Type as Type3 } from "@mariozechner/pi-ai";
107025
+ import { Type as Type3 } from "@earendil-works/pi-ai";
106549
107026
  import { exec as exec30 } from "node:child_process";
106550
107027
  import { readFile as readFile21 } from "node:fs/promises";
106551
107028
  import { join as join44 } from "node:path";
@@ -106604,9 +107081,9 @@ async function readAttachmentContents(rootDir, taskId, attachments) {
106604
107081
  return { attachmentContents, imageContents };
106605
107082
  }
106606
107083
  const { readFile: readFile29 } = await import("node:fs/promises");
106607
- const { join: join58 } = await import("node:path");
107084
+ const { join: join59 } = await import("node:path");
106608
107085
  for (const att of attachments) {
106609
- const filePath = join58(
107086
+ const filePath = join59(
106610
107087
  rootDir,
106611
107088
  ".fusion",
106612
107089
  "tasks",
@@ -108079,8 +108556,8 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
108079
108556
  });
108080
108557
  if (decision.shouldRetry) {
108081
108558
  const attempt = decision.nextState.recoveryRetryCount;
108082
- const delay4 = formatDelay(decision.delayMs);
108083
- const retryMessage = `Spec review not approved (${verdictDesc}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}.`;
108559
+ const delay5 = formatDelay(decision.delayMs);
108560
+ const retryMessage = `Spec review not approved (${verdictDesc}) \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay5}.`;
108084
108561
  planLog.warn(`${task.id} ${retryMessage}`);
108085
108562
  await this.store.logEntry(task.id, retryMessage);
108086
108563
  const restoreStatus = task.status === "needs-replan" ? "needs-replan" : null;
@@ -108186,10 +108663,10 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
108186
108663
  });
108187
108664
  if (decision.shouldRetry) {
108188
108665
  const attempt = decision.nextState.recoveryRetryCount;
108189
- const delay4 = formatDelay(decision.delayMs);
108666
+ const delay5 = formatDelay(decision.delayMs);
108190
108667
  if (!isSilentTransientError(errorMessage)) {
108191
- planLog.warn(`\u26A1 ${task.id} transient error during triage \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
108192
- await this.store.logEntry(task.id, `Transient error during specification (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`).catch((err2) => {
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) => {
108193
108670
  const msg = err2 instanceof Error ? err2.message : String(err2);
108194
108671
  planLog.warn(`${task.id}: failed to log transient-error retry entry: ${msg}`);
108195
108672
  });
@@ -108488,9 +108965,9 @@ Remove or replace these ids and call fn_task_create again.`
108488
108965
  }
108489
108966
  try {
108490
108967
  const { readFile: readFile29 } = await import("node:fs/promises");
108491
- const { join: join58 } = await import("node:path");
108968
+ const { join: join59 } = await import("node:path");
108492
108969
  const promptContent = await readFile29(
108493
- join58(rootDir, promptPath),
108970
+ join59(rootDir, promptPath),
108494
108971
  "utf-8"
108495
108972
  ).catch((err) => {
108496
108973
  const msg = err instanceof Error ? err.message : String(err);
@@ -109636,7 +110113,7 @@ var init_task_completion = __esm({
109636
110113
  import { spawn as spawn6 } from "node:child_process";
109637
110114
  import { existsSync as existsSync40 } from "node:fs";
109638
110115
  import { isAbsolute as isAbsolute13, join as join46 } from "node:path";
109639
- import { Type as Type4 } from "@mariozechner/pi-ai";
110116
+ import { Type as Type4 } from "@earendil-works/pi-ai";
109640
110117
  function createBuffer() {
109641
110118
  return { headChunks: [], headBytes: 0, tailChunks: [], tailBytes: 0, totalBytes: 0 };
109642
110119
  }
@@ -109945,8 +110422,8 @@ import { setImmediate as setImmediateCb3 } from "node:timers";
109945
110422
  import { delimiter, isAbsolute as isAbsolute14, join as join47, relative as relative11, resolve as resolvePath } from "node:path";
109946
110423
  import { existsSync as existsSync41, realpathSync as realpathSync6 } from "node:fs";
109947
110424
  import { readFile as readFile22, rm as rm5, writeFile as writeFile16 } from "node:fs/promises";
109948
- import { Type as Type5 } from "@mariozechner/pi-ai";
109949
- import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
110425
+ import { Type as Type5 } from "@earendil-works/pi-ai";
110426
+ import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@earendil-works/pi-coding-agent";
109950
110427
  function getResumeOrphanDelayMs() {
109951
110428
  const raw = process.env.FUSION_RESUME_ORPHAN_DELAY_MS;
109952
110429
  if (raw !== void 0) {
@@ -109963,6 +110440,26 @@ function canonicalizePath2(path6) {
109963
110440
  return resolvePath(path6);
109964
110441
  }
109965
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
+ }
109966
110463
  function detectPendingReviewBlock(task, codeReviewVerdicts) {
109967
110464
  const inProgressStepIndices = [];
109968
110465
  for (let stepIndex = 0; stepIndex < task.steps.length; stepIndex++) {
@@ -110498,7 +110995,7 @@ function detectReviewHandoffIntent(commentText) {
110498
110995
  ];
110499
110996
  return handoffPhrases.some((phrase) => text.includes(phrase));
110500
110997
  }
110501
- var execAsync30, yieldEventLoop3, tokenCacheMetricsLog, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, TASK_DONE_REFUSAL_SUFFIX, DISSENT_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, WORKFLOW_FEEDBACK_PATH_REGEX, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, WORKFLOW_STEP_VERDICTS, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
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;
110502
110999
  var init_executor = __esm({
110503
111000
  "../engine/src/executor.ts"() {
110504
111001
  "use strict";
@@ -110566,6 +111063,7 @@ var init_executor = __esm({
110566
111063
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
110567
111064
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
110568
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)'/;
110569
111067
  DISSENT_PATTERNS = [
110570
111068
  /\btask (is|was)(?: not|n['’]?t) complete\b/i,
110571
111069
  /\b(?:i (?:could|can)(?:not|n['’]?t)|unable to|failed to) (?:complete|finish|implement)\b/i,
@@ -113057,10 +113555,10 @@ ${summary}`,
113057
113555
  });
113058
113556
  if (decision.shouldRetry) {
113059
113557
  const attempt = decision.nextState.recoveryRetryCount;
113060
- const delay4 = formatDelay(decision.delayMs);
113558
+ const delay5 = formatDelay(decision.delayMs);
113061
113559
  if (!isSilentTransientError(errorMessage)) {
113062
- executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
113063
- await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
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));
113064
113562
  }
113065
113563
  if (worktreePath && existsSync41(worktreePath)) {
113066
113564
  try {
@@ -114022,9 +114520,9 @@ ${summary}`,
114022
114520
  });
114023
114521
  if (decision.shouldRetry) {
114024
114522
  const attempt = decision.nextState.recoveryRetryCount;
114025
- const delay4 = formatDelay(decision.delayMs);
114026
- executorLog.warn(`\u26A1 ${task.id} context-overflow fresh-session requeue ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}`);
114027
- await this.store.logEntry(task.id, `Context-overflow fresh-session requeue (${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
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));
114028
114526
  await this.store.updateTask(task.id, {
114029
114527
  recoveryRetryCount: decision.nextState.recoveryRetryCount,
114030
114528
  nextRecoveryAt: decision.nextState.nextRecoveryAt,
@@ -114312,10 +114810,10 @@ ${summary}`,
114312
114810
  });
114313
114811
  if (decision.shouldRetry) {
114314
114812
  const attempt = decision.nextState.recoveryRetryCount;
114315
- const delay4 = formatDelay(decision.delayMs);
114813
+ const delay5 = formatDelay(decision.delayMs);
114316
114814
  if (!isSilentTransientError(errorMessage)) {
114317
- executorLog.warn(`\u26A1 ${task.id} transient error \u2014 retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}: ${errorMessage}`);
114318
- await this.store.logEntry(task.id, `Transient error (retry ${attempt}/${MAX_RECOVERY_RETRIES} in ${delay4}): ${errorMessage}`, void 0, this.getRunContextFor(task.id));
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));
114319
114817
  }
114320
114818
  if (worktreePath && existsSync41(worktreePath)) {
114321
114819
  try {
@@ -117017,8 +117515,8 @@ Review the work done in this worktree and evaluate it against the criteria in yo
117017
117515
  `Failed to create worktree after ${this.MAX_WORKTREE_RETRIES} attempts: ${errorMessage}`
117018
117516
  );
117019
117517
  }
117020
- const delay4 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
117021
- await new Promise((resolve32) => setTimeout(resolve32, delay4));
117518
+ const delay5 = this.WORKTREE_RETRY_DELAYS[attempt] || 1e3;
117519
+ await new Promise((resolve32) => setTimeout(resolve32, delay5));
117022
117520
  }
117023
117521
  }
117024
117522
  throw new Error("Unexpected exit from worktree creation retry loop");
@@ -117256,9 +117754,15 @@ Review the work done in this worktree and evaluate it against the criteria in yo
117256
117754
  }
117257
117755
  async recoverMissingWorktreeSessionStartFailure(task, worktreePath, error, audit) {
117258
117756
  const errorText = error instanceof Error ? error.message : String(error);
117259
- if (!isMissingWorktreeSessionStartFailure(errorText)) return false;
117757
+ const missingWorktreeFailure = isMissingWorktreeSessionStartFailure(errorText);
117758
+ const missingTaskJsonFailure = isTransientMissingTaskJsonError(error, task);
117759
+ if (!missingWorktreeFailure && !missingTaskJsonFailure) return false;
117260
117760
  const classification = classifyMissingWorktreeSessionStartFailure(errorText);
117261
- 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
+ }
117262
117766
  await audit.git({
117263
117767
  type: "worktree:incomplete-detected",
117264
117768
  target: staleWorktreePath,
@@ -120414,6 +120918,12 @@ var init_scheduler = __esm({
120414
120918
  }
120415
120919
  const sliceIdBeforeUpdate = feature.sliceId;
120416
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
+ }
120417
120927
  void this.options.missionExecutionLoop.processTaskOutcome(taskId).catch((err) => {
120418
120928
  schedulerLog.error(`Error in missionExecutionLoop.processTaskOutcome for ${taskId}:`, err);
120419
120929
  });
@@ -120876,19 +121386,19 @@ var init_mission_autopilot = __esm({
120876
121386
  autopilotLog.error(`Error advancing slice for mission ${missionId}:`, err);
120877
121387
  state.retryCount++;
120878
121388
  if (state.retryCount <= MAX_RETRY_ATTEMPTS) {
120879
- 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);
120880
121390
  this.logMissionEventSafe(
120881
121391
  missionId,
120882
121392
  "autopilot_retry",
120883
121393
  `Retrying slice activation after error (attempt ${state.retryCount}/${MAX_RETRY_ATTEMPTS})`,
120884
- { retryCount: state.retryCount, maxRetries: MAX_RETRY_ATTEMPTS, delayMs: delay4 }
121394
+ { retryCount: state.retryCount, maxRetries: MAX_RETRY_ATTEMPTS, delayMs: delay5 }
120885
121395
  );
120886
- 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)`);
120887
121397
  setTimeout(() => {
120888
121398
  if (this.isWatching(missionId)) {
120889
121399
  void this.advanceToNextSlice(missionId);
120890
121400
  }
120891
- }, delay4);
121401
+ }, delay5);
120892
121402
  } else {
120893
121403
  this.logMissionEventSafe(
120894
121404
  missionId,
@@ -121335,7 +121845,7 @@ var init_mission_autopilot = __esm({
121335
121845
  });
121336
121846
 
121337
121847
  // ../engine/src/mission-execution-loop.ts
121338
- import { EventEmitter as EventEmitter18 } from "node:events";
121848
+ import { EventEmitter as EventEmitter19 } from "node:events";
121339
121849
  var loopLog, VALIDATION_TIMEOUT_MS, MissionExecutionLoop;
121340
121850
  var init_mission_execution_loop = __esm({
121341
121851
  "../engine/src/mission-execution-loop.ts"() {
@@ -121347,7 +121857,7 @@ var init_mission_execution_loop = __esm({
121347
121857
  init_run_audit();
121348
121858
  loopLog = createLogger2("mission-loop");
121349
121859
  VALIDATION_TIMEOUT_MS = 10 * 60 * 1e3;
121350
- MissionExecutionLoop = class extends EventEmitter18 {
121860
+ MissionExecutionLoop = class extends EventEmitter19 {
121351
121861
  running = false;
121352
121862
  taskStore;
121353
121863
  missionStore;
@@ -121487,6 +121997,10 @@ var init_mission_execution_loop = __esm({
121487
121997
  loopLog.log(`Feature ${feature.id} loopState is "${feature.loopState}"; skipping validation`);
121488
121998
  return;
121489
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
+ }
121490
122004
  const assertions = this.missionStore.listAssertionsForFeature(feature.id);
121491
122005
  if (assertions.length === 0) {
121492
122006
  loopLog.log(`Feature ${feature.id} has no linked assertions; marking as passed`);
@@ -121875,6 +122389,10 @@ ${taskContext}` : ""}`;
121875
122389
  if (runId) {
121876
122390
  this.missionStore.completeValidatorRun(runId, "passed", summary);
121877
122391
  }
122392
+ const feature = this.missionStore.getFeature(featureId);
122393
+ if (feature && feature.status !== "done") {
122394
+ this.missionStore.updateFeatureStatus(featureId, "done");
122395
+ }
121878
122396
  loopLog.log(`Feature ${featureId} passed validation`);
121879
122397
  if (this.missionAutopilot?.notifyValidationComplete) {
121880
122398
  await this.missionAutopilot.notifyValidationComplete(featureId, "passed");
@@ -123085,7 +123603,7 @@ var init_room_coordination = __esm({
123085
123603
  });
123086
123604
 
123087
123605
  // ../engine/src/agent-heartbeat.ts
123088
- import { Type as Type6 } from "@mariozechner/pi-ai";
123606
+ import { Type as Type6 } from "@earendil-works/pi-ai";
123089
123607
  import { createHash as createHash12 } from "node:crypto";
123090
123608
  function adjustHeartbeatMemoryPrimer(basePrompt, mode) {
123091
123609
  if (mode === "full") return basePrompt;
@@ -130216,7 +130734,7 @@ var init_stuck_task_detector = __esm({
130216
130734
  });
130217
130735
 
130218
130736
  // ../engine/src/plugin-runner.ts
130219
- import { Type as Type7 } from "@mariozechner/pi-ai";
130737
+ import { Type as Type7 } from "@earendil-works/pi-ai";
130220
130738
  import { isAbsolute as isAbsolute16 } from "node:path";
130221
130739
  var DEFAULT_HOOK_TIMEOUT_MS, PluginRunner;
130222
130740
  var init_plugin_runner = __esm({
@@ -131367,7 +131885,7 @@ var init_task_agent_sync = __esm({
131367
131885
  });
131368
131886
 
131369
131887
  // ../engine/src/runtimes/in-process-runtime.ts
131370
- import { EventEmitter as EventEmitter19 } from "node:events";
131888
+ import { EventEmitter as EventEmitter20 } from "node:events";
131371
131889
  import { setImmediate as setImmediateCb4 } from "node:timers";
131372
131890
  var yieldEventLoop4, InProcessRuntime;
131373
131891
  var init_in_process_runtime = __esm({
@@ -131397,7 +131915,7 @@ var init_in_process_runtime = __esm({
131397
131915
  init_task_agent_sync();
131398
131916
  init_run_audit();
131399
131917
  yieldEventLoop4 = () => new Promise((resolve32) => setImmediateCb4(resolve32));
131400
- InProcessRuntime = class extends EventEmitter19 {
131918
+ InProcessRuntime = class extends EventEmitter20 {
131401
131919
  /**
131402
131920
  * @param config - Runtime configuration
131403
131921
  * @param centralCore - CentralCore reference for global coordination
@@ -132427,14 +132945,14 @@ var init_ipc_protocol = __esm({
132427
132945
  });
132428
132946
 
132429
132947
  // ../engine/src/ipc/ipc-host.ts
132430
- import { EventEmitter as EventEmitter20 } from "node:events";
132948
+ import { EventEmitter as EventEmitter21 } from "node:events";
132431
132949
  var IpcHost;
132432
132950
  var init_ipc_host = __esm({
132433
132951
  "../engine/src/ipc/ipc-host.ts"() {
132434
132952
  "use strict";
132435
132953
  init_ipc_protocol();
132436
132954
  init_logger2();
132437
- IpcHost = class extends EventEmitter20 {
132955
+ IpcHost = class extends EventEmitter21 {
132438
132956
  /**
132439
132957
  * @param childProcess - The forked child process to communicate with
132440
132958
  * @param options - Optional configuration
@@ -132624,7 +133142,7 @@ var child_process_runtime_exports = {};
132624
133142
  __export(child_process_runtime_exports, {
132625
133143
  ChildProcessRuntime: () => ChildProcessRuntime
132626
133144
  });
132627
- import { EventEmitter as EventEmitter21 } from "node:events";
133145
+ import { EventEmitter as EventEmitter22 } from "node:events";
132628
133146
  import { fork } from "node:child_process";
132629
133147
  import { fileURLToPath as fileURLToPath3 } from "node:url";
132630
133148
  import { dirname as dirname18, join as join49 } from "node:path";
@@ -132681,8 +133199,8 @@ var init_child_process_runtime = __esm({
132681
133199
  runtimeLog.log("Health monitor stopped");
132682
133200
  }
132683
133201
  getRestartDelay() {
132684
- const delay4 = this.restartDelays[this.restartAttempts] ?? this.restartDelays[this.restartDelays.length - 1];
132685
- return delay4;
133202
+ const delay5 = this.restartDelays[this.restartAttempts] ?? this.restartDelays[this.restartDelays.length - 1];
133203
+ return delay5;
132686
133204
  }
132687
133205
  incrementRestartAttempts() {
132688
133206
  this.restartAttempts++;
@@ -132694,7 +133212,7 @@ var init_child_process_runtime = __esm({
132694
133212
  return this.missedHeartbeats;
132695
133213
  }
132696
133214
  };
132697
- ChildProcessRuntime = class extends EventEmitter21 {
133215
+ ChildProcessRuntime = class extends EventEmitter22 {
132698
133216
  /**
132699
133217
  * @param config - Runtime configuration
132700
133218
  * @param centralCore - CentralCore reference for global coordination
@@ -132963,9 +133481,9 @@ var init_child_process_runtime = __esm({
132963
133481
  this.emit("error", new Error("Child process failed after max restart attempts"));
132964
133482
  return;
132965
133483
  }
132966
- const delay4 = this.healthMonitor.getRestartDelay();
133484
+ const delay5 = this.healthMonitor.getRestartDelay();
132967
133485
  this.healthMonitor.incrementRestartAttempts();
132968
- runtimeLog.log(`Attempting restart ${this.healthMonitor.getRestartAttempts()}/${maxRestarts} after ${delay4}ms`);
133486
+ runtimeLog.log(`Attempting restart ${this.healthMonitor.getRestartAttempts()}/${maxRestarts} after ${delay5}ms`);
132969
133487
  const gen = this.generation;
132970
133488
  this.restartTimer = setTimeout(async () => {
132971
133489
  this.restartTimer = null;
@@ -132984,7 +133502,7 @@ var init_child_process_runtime = __esm({
132984
133502
  this.setStatus("errored");
132985
133503
  this.emit("error", error instanceof Error ? error : new Error(String(error)));
132986
133504
  }
132987
- }, delay4);
133505
+ }, delay5);
132988
133506
  }
132989
133507
  /**
132990
133508
  * Handle child process exit.
@@ -133379,14 +133897,14 @@ var init_remote_node_client = __esm({
133379
133897
  });
133380
133898
 
133381
133899
  // ../engine/src/runtimes/remote-node-runtime.ts
133382
- import { EventEmitter as EventEmitter22 } from "node:events";
133900
+ import { EventEmitter as EventEmitter23 } from "node:events";
133383
133901
  var RemoteNodeRuntime;
133384
133902
  var init_remote_node_runtime = __esm({
133385
133903
  "../engine/src/runtimes/remote-node-runtime.ts"() {
133386
133904
  "use strict";
133387
133905
  init_logger2();
133388
133906
  init_remote_node_client();
133389
- RemoteNodeRuntime = class extends EventEmitter22 {
133907
+ RemoteNodeRuntime = class extends EventEmitter23 {
133390
133908
  constructor(config) {
133391
133909
  super();
133392
133910
  this.config = config;
@@ -133696,7 +134214,7 @@ var init_remote_node_runtime = __esm({
133696
134214
  });
133697
134215
 
133698
134216
  // ../engine/src/project-manager.ts
133699
- import { EventEmitter as EventEmitter23 } from "node:events";
134217
+ import { EventEmitter as EventEmitter24 } from "node:events";
133700
134218
  var ProjectManager;
133701
134219
  var init_project_manager = __esm({
133702
134220
  "../engine/src/project-manager.ts"() {
@@ -133706,7 +134224,7 @@ var init_project_manager = __esm({
133706
134224
  init_remote_node_runtime();
133707
134225
  init_concurrency();
133708
134226
  init_logger2();
133709
- ProjectManager = class extends EventEmitter23 {
134227
+ ProjectManager = class extends EventEmitter24 {
133710
134228
  /**
133711
134229
  * @param centralCore - CentralCore reference for global coordination
133712
134230
  */
@@ -134173,6 +134691,653 @@ var init_gridlock_detector = __esm({
134173
134691
  }
134174
134692
  });
134175
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
+
134176
135341
  // ../engine/src/research-dispatcher.ts
134177
135342
  var log24, ResearchRunDispatcher;
134178
135343
  var init_research_dispatcher = __esm({
@@ -134415,9 +135580,9 @@ var init_provider_adapters = __esm({
134415
135580
  });
134416
135581
 
134417
135582
  // ../engine/src/remote-access/tunnel-process-manager.ts
134418
- import { EventEmitter as EventEmitter24 } from "node:events";
134419
- import { exec as exec36, execFile as execFile15, spawn as spawn8 } from "node:child_process";
134420
- import { promisify as promisify43 } from "node:util";
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";
134421
135586
  function nowIso2() {
134422
135587
  return (/* @__PURE__ */ new Date()).toISOString();
134423
135588
  }
@@ -134457,7 +135622,7 @@ function toStateError(code, err) {
134457
135622
  at: nowIso2()
134458
135623
  };
134459
135624
  }
134460
- 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;
134461
135626
  var init_tunnel_process_manager = __esm({
134462
135627
  "../engine/src/remote-access/tunnel-process-manager.ts"() {
134463
135628
  "use strict";
@@ -134466,8 +135631,8 @@ var init_tunnel_process_manager = __esm({
134466
135631
  init_provider_adapters();
134467
135632
  DEFAULT_MAX_LOG_ENTRIES = 400;
134468
135633
  DEFAULT_STOP_TIMEOUT_MS2 = 5e3;
134469
- execFileAsync10 = promisify43(execFile15);
134470
- execAsync32 = promisify43(exec36);
135634
+ execFileAsync11 = promisify44(execFile16);
135635
+ execAsync32 = promisify44(exec36);
134471
135636
  LineBuffer = class {
134472
135637
  pending = "";
134473
135638
  push(chunk) {
@@ -134482,7 +135647,7 @@ var init_tunnel_process_manager = __esm({
134482
135647
  return tail ? [tail] : [];
134483
135648
  }
134484
135649
  };
134485
- TunnelProcessManager = class extends EventEmitter24 {
135650
+ TunnelProcessManager = class extends EventEmitter25 {
134486
135651
  maxLogEntries;
134487
135652
  defaultStopTimeoutMs;
134488
135653
  spawnImpl;
@@ -134544,7 +135709,7 @@ var init_tunnel_process_manager = __esm({
134544
135709
  return null;
134545
135710
  }
134546
135711
  try {
134547
- const { stdout } = await execFileAsync10("tailscale", ["status", "--json"], { timeout: 3e3 });
135712
+ const { stdout } = await execFileAsync11("tailscale", ["status", "--json"], { timeout: 3e3 });
134548
135713
  const data = JSON.parse(String(stdout));
134549
135714
  const dnsName = data.Self?.DNSName?.replace(/\.$/, "");
134550
135715
  if (!dnsName) {
@@ -134567,7 +135732,7 @@ var init_tunnel_process_manager = __esm({
134567
135732
  ];
134568
135733
  for (const resetCommand of resetCommands) {
134569
135734
  try {
134570
- await execFileAsync10(resetCommand.command, resetCommand.args, { timeout: 5e3 });
135735
+ await execFileAsync11(resetCommand.command, resetCommand.args, { timeout: 5e3 });
134571
135736
  return;
134572
135737
  } catch {
134573
135738
  }
@@ -134868,8 +136033,8 @@ var init_tunnel_process_manager = __esm({
134868
136033
  });
134869
136034
 
134870
136035
  // ../engine/src/project-engine.ts
134871
- import { execFile as execFile16 } from "node:child_process";
134872
- import { promisify as promisify44 } from "node:util";
136036
+ import { execFile as execFile17 } from "node:child_process";
136037
+ import { promisify as promisify45 } from "node:util";
134873
136038
  function formatErrorDetails(error) {
134874
136039
  if (error instanceof Error) {
134875
136040
  return {
@@ -134884,6 +136049,40 @@ function isInvalidDoneTransitionError(error) {
134884
136049
  const message = error instanceof Error ? error.message : String(error);
134885
136050
  return message.includes("Invalid transition:") && message.includes("\u2192 'done'");
134886
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
+ }
134887
136086
  function buildVerificationFailureSignature(error) {
134888
136087
  const commandResult = error.verificationResult.testResult ?? error.verificationResult.buildResult;
134889
136088
  const lane = commandResult?.command?.trim() || error.verificationResult.failedCommand?.trim() || "verification-failure";
@@ -134894,7 +136093,7 @@ function buildVerificationFailureSignature(error) {
134894
136093
  failedCommand: commandResult?.command ?? error.verificationResult.failedCommand ?? null
134895
136094
  }).signature;
134896
136095
  }
134897
- var execFileAsync11, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
136096
+ var execFileAsync12, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
134898
136097
  var init_project_engine = __esm({
134899
136098
  "../engine/src/project-engine.ts"() {
134900
136099
  "use strict";
@@ -134908,6 +136107,7 @@ var init_project_engine = __esm({
134908
136107
  init_auth_storage();
134909
136108
  init_cron_runner();
134910
136109
  init_merger();
136110
+ init_merger_ai();
134911
136111
  init_concurrency();
134912
136112
  init_logger2();
134913
136113
  init_research_orchestrator();
@@ -134916,7 +136116,7 @@ var init_project_engine = __esm({
134916
136116
  init_run_audit();
134917
136117
  init_verification_followup_dedup();
134918
136118
  init_tunnel_process_manager();
134919
- execFileAsync11 = promisify44(execFile16);
136119
+ execFileAsync12 = promisify45(execFile17);
134920
136120
  MERGE_HANDOFF_GRACE_MS = 300;
134921
136121
  isRemoteActive = (ra) => ra?.activeProvider != null && (ra.providers[ra.activeProvider]?.enabled ?? false);
134922
136122
  ProjectEngine = class _ProjectEngine {
@@ -135661,7 +136861,7 @@ ${detail}`
135661
136861
  async checkExecutableAvailable(command) {
135662
136862
  const checker = process.platform === "win32" ? "where" : "which";
135663
136863
  try {
135664
- await execFileAsync11(checker, [command]);
136864
+ await execFileAsync12(checker, [command]);
135665
136865
  return { available: true };
135666
136866
  } catch {
135667
136867
  return {
@@ -135861,6 +137061,112 @@ ${detail}`
135861
137061
  continue;
135862
137062
  }
135863
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
+ }
135864
137170
  const blockerReason = getTaskHardMergeBlocker(task);
135865
137171
  if (blockerReason) {
135866
137172
  await store.updateTask(taskId, {
@@ -135996,10 +137302,10 @@ ${detail}`
135996
137302
  const pool = this.runtime.worktreePool;
135997
137303
  const agentStore = this.runtime.agentStore;
135998
137304
  const usageLimitPauser = this.runtime.usageLimitPauser;
135999
- const rawMerge = () => {
137305
+ const rawMerge = async () => {
136000
137306
  this.activeMergeTaskId = taskId;
136001
137307
  this.mergeAbortController = new AbortController();
136002
- return aiMergeTask(store, cwd, taskId, {
137308
+ const mergerOptions = {
136003
137309
  manual: !!manualResolver,
136004
137310
  pool,
136005
137311
  usageLimitPauser,
@@ -136008,7 +137314,9 @@ ${detail}`
136008
137314
  onSession: (session) => {
136009
137315
  this.activeMergeSession = session;
136010
137316
  }
136011
- });
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);
136012
137320
  };
136013
137321
  let result;
136014
137322
  if (semaphore) {
@@ -138564,8 +139872,8 @@ var require_proper_lockfile = __commonJS({
138564
139872
  import { mkdirSync as mkdirSync10 } from "node:fs";
138565
139873
  import { access as access7, unlink as unlink7, writeFile as writeFile18 } from "node:fs/promises";
138566
139874
  import net from "node:net";
138567
- import { platform as platform3, tmpdir as tmpdir5 } from "node:os";
138568
- 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";
138569
139877
  import { createHash as createHash14 } from "node:crypto";
138570
139878
  function projectHash(projectId) {
138571
139879
  return createHash14("sha1").update(projectId).digest("hex").slice(0, 16);
@@ -138575,10 +139883,10 @@ function computeEngineSocketPath(projectId) {
138575
139883
  if (platform3() === "win32") {
138576
139884
  return `\\\\.\\pipe\\fusion-engine-${hash}`;
138577
139885
  }
138578
- return join50(tmpdir5(), `fusion-engine-${hash}.sock`);
139886
+ return join51(tmpdir6(), `fusion-engine-${hash}.sock`);
138579
139887
  }
138580
139888
  function computeEngineLockFilePath(workingDir) {
138581
- return join50(workingDir, ".fusion", "engine.lock");
139889
+ return join51(workingDir, ".fusion", "engine.lock");
138582
139890
  }
138583
139891
  async function ensureLockTargetExists(lockPath) {
138584
139892
  try {
@@ -138588,7 +139896,7 @@ async function ensureLockTargetExists(lockPath) {
138588
139896
  }
138589
139897
  }
138590
139898
  async function acquireLockfile(workingDir, onCompromised) {
138591
- const dir = join50(workingDir, ".fusion");
139899
+ const dir = join51(workingDir, ".fusion");
138592
139900
  mkdirSync10(dir, { recursive: true });
138593
139901
  const path6 = computeEngineLockFilePath(workingDir);
138594
139902
  await ensureLockTargetExists(path6);
@@ -139204,7 +140512,7 @@ var init_node_health_monitor = __esm({
139204
140512
  });
139205
140513
 
139206
140514
  // ../engine/src/hybrid-executor.ts
139207
- import { EventEmitter as EventEmitter25 } from "node:events";
140515
+ import { EventEmitter as EventEmitter26 } from "node:events";
139208
140516
  var HybridExecutor;
139209
140517
  var init_hybrid_executor = __esm({
139210
140518
  "../engine/src/hybrid-executor.ts"() {
@@ -139212,7 +140520,7 @@ var init_hybrid_executor = __esm({
139212
140520
  init_project_manager();
139213
140521
  init_node_health_monitor();
139214
140522
  init_logger2();
139215
- HybridExecutor = class extends EventEmitter25 {
140523
+ HybridExecutor = class extends EventEmitter26 {
139216
140524
  /**
139217
140525
  * @param centralCore - CentralCore reference for global coordination
139218
140526
  * @param options - Optional configuration callbacks
@@ -140522,7 +141830,7 @@ var init_planning_board_tools = __esm({
140522
141830
 
140523
141831
  // ../dashboard/src/planning.ts
140524
141832
  import { randomUUID as randomUUID22 } from "node:crypto";
140525
- import { EventEmitter as EventEmitter26 } from "node:events";
141833
+ import { EventEmitter as EventEmitter27 } from "node:events";
140526
141834
  function ensureEngineReady() {
140527
141835
  return Promise.resolve();
140528
141836
  }
@@ -141546,7 +142854,7 @@ For completion:
141546
142854
  process.on("beforeExit", () => {
141547
142855
  clearInterval(cleanupInterval2);
141548
142856
  });
141549
- PlanningStreamManager = class extends EventEmitter26 {
142857
+ PlanningStreamManager = class extends EventEmitter27 {
141550
142858
  constructor(bufferSize = 100) {
141551
142859
  super();
141552
142860
  this.bufferSize = bufferSize;
@@ -141761,7 +143069,7 @@ var init_ai_session_timeout = __esm({
141761
143069
  });
141762
143070
 
141763
143071
  // ../dashboard/src/subtask-breakdown.ts
141764
- import { EventEmitter as EventEmitter27 } from "node:events";
143072
+ import { EventEmitter as EventEmitter28 } from "node:events";
141765
143073
  function cleanupInMemorySubtaskSession(sessionId) {
141766
143074
  const session = sessions2.get(sessionId);
141767
143075
  if (!session) {
@@ -141803,7 +143111,7 @@ var init_subtask_breakdown = __esm({
141803
143111
  process.on("beforeExit", () => {
141804
143112
  clearInterval(cleanupInterval3);
141805
143113
  });
141806
- SubtaskStreamManager = class extends EventEmitter27 {
143114
+ SubtaskStreamManager = class extends EventEmitter28 {
141807
143115
  constructor(bufferSize = 100) {
141808
143116
  super();
141809
143117
  this.bufferSize = bufferSize;
@@ -141865,7 +143173,7 @@ var init_subtask_breakdown = __esm({
141865
143173
  });
141866
143174
 
141867
143175
  // ../dashboard/src/mission-interview.ts
141868
- import { EventEmitter as EventEmitter28 } from "node:events";
143176
+ import { EventEmitter as EventEmitter29 } from "node:events";
141869
143177
  function cleanupInMemoryMissionSession(sessionId) {
141870
143178
  const session = sessions3.get(sessionId);
141871
143179
  if (!session) {
@@ -141916,7 +143224,7 @@ var init_mission_interview = __esm({
141916
143224
  cleanupInterval4 = setInterval(cleanupExpiredSessions3, CLEANUP_INTERVAL_MS4);
141917
143225
  cleanupInterval4.unref?.();
141918
143226
  process.on("beforeExit", () => clearInterval(cleanupInterval4));
141919
- MissionInterviewStreamManager = class extends EventEmitter28 {
143227
+ MissionInterviewStreamManager = class extends EventEmitter29 {
141920
143228
  constructor(bufferSize = 100) {
141921
143229
  super();
141922
143230
  this.bufferSize = bufferSize;
@@ -141984,7 +143292,7 @@ var init_mission_interview = __esm({
141984
143292
  });
141985
143293
 
141986
143294
  // ../dashboard/src/milestone-slice-interview.ts
141987
- import { EventEmitter as EventEmitter29 } from "node:events";
143295
+ import { EventEmitter as EventEmitter30 } from "node:events";
141988
143296
  function cleanupInMemorySession2(sessionId) {
141989
143297
  const session = sessions4.get(sessionId);
141990
143298
  if (!session) {
@@ -142036,7 +143344,7 @@ var init_milestone_slice_interview = __esm({
142036
143344
  cleanupInterval5 = setInterval(cleanupExpiredSessions4, CLEANUP_INTERVAL_MS5);
142037
143345
  cleanupInterval5.unref?.();
142038
143346
  process.on("beforeExit", () => clearInterval(cleanupInterval5));
142039
- MilestoneSliceInterviewStreamManager = class extends EventEmitter29 {
143347
+ MilestoneSliceInterviewStreamManager = class extends EventEmitter30 {
142040
143348
  constructor(bufferSize = 100) {
142041
143349
  super();
142042
143350
  this.bufferSize = bufferSize;
@@ -142351,13 +143659,13 @@ var init_cli_spawn = __esm({
142351
143659
  // ../../plugins/fusion-plugin-hermes-runtime/dist/fusion-skill-install.js
142352
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";
142353
143661
  import { homedir as homedir8 } from "node:os";
142354
- 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";
142355
143663
  import { fileURLToPath as fileURLToPath4 } from "node:url";
142356
143664
  function resolveHermesHome(profile) {
142357
- const base = process.env.HERMES_HOME ?? join51(homedir8(), ".hermes");
143665
+ const base = process.env.HERMES_HOME ?? join52(homedir8(), ".hermes");
142358
143666
  if (!profile || profile === "default")
142359
143667
  return base;
142360
- return join51(base, "profiles", profile);
143668
+ return join52(base, "profiles", profile);
142361
143669
  }
142362
143670
  function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
142363
143671
  const here = fileURLToPath4(moduleUrl);
@@ -142372,14 +143680,14 @@ function getFusionSkillSourceCandidates(moduleUrl = import.meta.url) {
142372
143680
  function resolveBundledFusionSkillSource() {
142373
143681
  const candidates = getFusionSkillSourceCandidates();
142374
143682
  for (const candidate of candidates) {
142375
- if (existsSync43(join51(candidate, "SKILL.md")))
143683
+ if (existsSync43(join52(candidate, "SKILL.md")))
142376
143684
  return candidate;
142377
143685
  }
142378
143686
  return null;
142379
143687
  }
142380
143688
  function installFusionSkillIntoHermesHome(options = {}) {
142381
143689
  const sourceDir = options.sourceDir ?? resolveBundledFusionSkillSource();
142382
- const targetDir = join51(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
143690
+ const targetDir = join52(resolveHermesHome(options.profile), "skills", FUSION_SKILL_NAME);
142383
143691
  if (!sourceDir) {
142384
143692
  return {
142385
143693
  outcome: "warning",
@@ -142468,7 +143776,7 @@ function isBrokenSymlink(path6) {
142468
143776
  }
142469
143777
  }
142470
143778
  function looksLikePriorFusionInstall(path6) {
142471
- const skillMd = join51(path6, "SKILL.md");
143779
+ const skillMd = join52(path6, "SKILL.md");
142472
143780
  if (!existsSync43(skillMd))
142473
143781
  return false;
142474
143782
  try {
@@ -142483,7 +143791,7 @@ function looksLikeFusionSkillTarget(path6) {
142483
143791
  return false;
142484
143792
  if (basename14(path6).toLowerCase() === FUSION_SKILL_NAME)
142485
143793
  return true;
142486
- return existsSync43(join51(path6, "SKILL.md"));
143794
+ return existsSync43(join52(path6, "SKILL.md"));
142487
143795
  }
142488
143796
  var FUSION_SKILL_NAME;
142489
143797
  var init_fusion_skill_install = __esm({
@@ -142861,8 +144169,8 @@ var init_pi_module = __esm({
142861
144169
 
142862
144170
  // ../../plugins/fusion-plugin-openclaw-runtime/dist/mcp-config.js
142863
144171
  import { writeFileSync as writeFileSync5 } from "node:fs";
142864
- import { tmpdir as tmpdir6 } from "node:os";
142865
- import { join as join52 } from "node:path";
144172
+ import { tmpdir as tmpdir7 } from "node:os";
144173
+ import { join as join53 } from "node:path";
142866
144174
  import { fileURLToPath as fileURLToPath5 } from "node:url";
142867
144175
  import { dirname as dirname20 } from "node:path";
142868
144176
  function toolsToMcpToolDefs(tools) {
@@ -142876,16 +144184,16 @@ function toolsToMcpToolDefs(tools) {
142876
144184
  }
142877
144185
  function writeOpenClawMcpBridgeFiles(toolDefs, cacheKey) {
142878
144186
  const suffix = cacheKey ? `${process.pid}-${cacheKey}` : `${process.pid}`;
142879
- const schemaPath = join52(tmpdir6(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
144187
+ const schemaPath = join53(tmpdir7(), `openclaw-runtime-mcp-schemas-${suffix}.json`);
142880
144188
  writeFileSync5(schemaPath, JSON.stringify(toolDefs));
142881
144189
  const __filename = fileURLToPath5(import.meta.url);
142882
144190
  const __dirname2 = dirname20(__filename);
142883
- const serverPath = join52(__dirname2, "mcp-schema-server.cjs");
144191
+ const serverPath = join53(__dirname2, "mcp-schema-server.cjs");
142884
144192
  const serverConfig = {
142885
144193
  command: "node",
142886
144194
  args: [serverPath, schemaPath]
142887
144195
  };
142888
- const serverConfigPath = join52(tmpdir6(), `openclaw-runtime-mcp-server-${suffix}.json`);
144196
+ const serverConfigPath = join53(tmpdir7(), `openclaw-runtime-mcp-server-${suffix}.json`);
142889
144197
  writeFileSync5(serverConfigPath, JSON.stringify(serverConfig));
142890
144198
  return {
142891
144199
  schemaPath,
@@ -143130,10 +144438,10 @@ var init_context = __esm({
143130
144438
 
143131
144439
  // ../dashboard/src/github.ts
143132
144440
  import { exec as exec37 } from "node:child_process";
143133
- import { mkdtemp as mkdtemp2, rm as rm6 } from "node:fs/promises";
143134
- import { tmpdir as tmpdir7 } from "node:os";
143135
- import { join as join53 } from "node:path";
143136
- import { promisify as promisify45 } from "node:util";
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";
143137
144445
  function quoteGitArg(value) {
143138
144446
  return `'${value.replaceAll("'", "'\\''")}'`;
143139
144447
  }
@@ -143391,7 +144699,7 @@ var init_github = __esm({
143391
144699
  "../dashboard/src/github.ts"() {
143392
144700
  "use strict";
143393
144701
  init_src();
143394
- execAsync33 = promisify45(exec37);
144702
+ execAsync33 = promisify46(exec37);
143395
144703
  PR_REVIEW_PAGE_SIZE = 100;
143396
144704
  MAX_PR_REVIEW_PAGES = 10;
143397
144705
  MAX_BADGE_BATCH_SIZE = 100;
@@ -144059,8 +145367,8 @@ var init_github = __esm({
144059
145367
  { timeout: 3e4, maxBuffer: 10 * 1024 * 1024 }
144060
145368
  );
144061
145369
  const mergeBase = mergeBaseStdout.trim();
144062
- const indexDir = await mkdtemp2(join53(tmpdir7(), "fn-pr-conflict-"));
144063
- const indexPath = join53(indexDir, "index");
145370
+ const indexDir = await mkdtemp3(join54(tmpdir8(), "fn-pr-conflict-"));
145371
+ const indexPath = join54(indexDir, "index");
144064
145372
  const gitEnv = { ...process.env, GIT_INDEX_FILE: indexPath };
144065
145373
  try {
144066
145374
  await execAsync33(`git -C ${quoteGitArg(opts.repoRoot)} read-tree -m ${quoteGitArg(mergeBase)} ${quoteGitArg(baseRef)} ${quoteGitArg(headRef)}`, {
@@ -144079,7 +145387,7 @@ var init_github = __esm({
144079
145387
  )
144080
145388
  ];
144081
145389
  } finally {
144082
- await rm6(indexDir, { recursive: true, force: true });
145390
+ await rm7(indexDir, { recursive: true, force: true });
144083
145391
  }
144084
145392
  } catch {
144085
145393
  conflictingFiles = [];
@@ -146111,8 +147419,18 @@ var init_github_tracking_reconciler = __esm({
146111
147419
  }
146112
147420
  });
146113
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
+
146114
147432
  // ../dashboard/src/github-poll.ts
146115
- import { EventEmitter as EventEmitter30 } from "node:events";
147433
+ import { EventEmitter as EventEmitter31 } from "node:events";
146116
147434
  function toAlias(type, number) {
146117
147435
  return `${type}_${number}`;
146118
147436
  }
@@ -146158,7 +147476,7 @@ var init_github_poll = __esm({
146158
147476
  }
146159
147477
  };
146160
147478
  githubRateLimiter = new GitHubRateLimiter();
146161
- GitHubPollingService = class extends EventEmitter30 {
147479
+ GitHubPollingService = class extends EventEmitter31 {
146162
147480
  watches = /* @__PURE__ */ new Map();
146163
147481
  rateLimiter;
146164
147482
  pollingIntervalMs;
@@ -146403,10 +147721,10 @@ var init_github_poll = __esm({
146403
147721
  });
146404
147722
 
146405
147723
  // ../dashboard/src/pr-metadata-generator.ts
146406
- import { promisify as promisify46 } from "node:util";
147724
+ import { promisify as promisify47 } from "node:util";
146407
147725
  import { exec as execCb } from "node:child_process";
146408
147726
  import { access as access8, readFile as readFile26 } from "node:fs/promises";
146409
- import { join as join54 } from "node:path";
147727
+ import { join as join55 } from "node:path";
146410
147728
  function buildFallback(task) {
146411
147729
  return {
146412
147730
  title: task.title ?? task.id,
@@ -146539,12 +147857,12 @@ async function generatePrMetadata(input) {
146539
147857
  ]);
146540
147858
  let promptContent = "";
146541
147859
  try {
146542
- const promptPath = join54(repoRoot, ".fusion", "tasks", task.id, "PROMPT.md");
147860
+ const promptPath = join55(repoRoot, ".fusion", "tasks", task.id, "PROMPT.md");
146543
147861
  promptContent = (await readFile26(promptPath, "utf8")).trim();
146544
147862
  } catch {
146545
147863
  promptContent = "";
146546
147864
  }
146547
- const templatePath = join54(repoRoot, ".github", "pull_request_template.md");
147865
+ const templatePath = join55(repoRoot, ".github", "pull_request_template.md");
146548
147866
  const templateExists = await access8(templatePath).then(() => true).catch(() => false);
146549
147867
  const template = templateExists ? await readFile26(templatePath, "utf8") : "";
146550
147868
  const model = resolveTaskPlanningModel(task, settings);
@@ -146603,24 +147921,24 @@ var init_pr_metadata_generator = __esm({
146603
147921
  "use strict";
146604
147922
  init_src();
146605
147923
  init_src2();
146606
- execAsync34 = promisify46(execCb);
147924
+ execAsync34 = promisify47(execCb);
146607
147925
  }
146608
147926
  });
146609
147927
 
146610
147928
  // ../dashboard/src/routes/resolve-diff-base.ts
146611
- import { execFile as execFile17 } from "node:child_process";
146612
- import { promisify as promisify47 } from "node:util";
146613
- var execFileAsync12;
147929
+ import { execFile as execFile18 } from "node:child_process";
147930
+ import { promisify as promisify48 } from "node:util";
147931
+ var execFileAsync13;
146614
147932
  var init_resolve_diff_base = __esm({
146615
147933
  "../dashboard/src/routes/resolve-diff-base.ts"() {
146616
147934
  "use strict";
146617
- execFileAsync12 = promisify47(execFile17);
147935
+ execFileAsync13 = promisify48(execFile18);
146618
147936
  }
146619
147937
  });
146620
147938
 
146621
147939
  // ../dashboard/src/routes/register-git-github.ts
146622
147940
  import { exec as execCb2, spawn as spawn12 } from "node:child_process";
146623
- import { promisify as promisify48 } from "node:util";
147941
+ import { promisify as promisify49 } from "node:util";
146624
147942
  var execAsync35, PR_ROUTE_MAX_BUFFER_BYTES;
146625
147943
  var init_register_git_github = __esm({
146626
147944
  "../dashboard/src/routes/register-git-github.ts"() {
@@ -146633,12 +147951,13 @@ var init_register_git_github = __esm({
146633
147951
  init_github_tracking_comments();
146634
147952
  init_github_tracking_state();
146635
147953
  init_github_tracking_reconciler();
147954
+ init_github_source_issue_close();
146636
147955
  init_github_poll();
146637
147956
  init_project_store_resolver();
146638
147957
  init_pr_metadata_generator();
146639
147958
  init_github_webhooks();
146640
147959
  init_resolve_diff_base();
146641
- execAsync35 = promisify48(execCb2);
147960
+ execAsync35 = promisify49(execCb2);
146642
147961
  PR_ROUTE_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
146643
147962
  }
146644
147963
  });
@@ -146677,8 +147996,8 @@ var init_register_planning_subtask_routes = __esm({
146677
147996
  });
146678
147997
 
146679
147998
  // ../dashboard/src/chat.ts
146680
- import { EventEmitter as EventEmitter31 } from "node:events";
146681
- 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";
146682
148001
  var defaultDiagnostics, _diagnostics, diagnostics6, RATE_LIMIT_WINDOW_MS5, MAX_REFERENCED_FILE_SIZE, ChatStreamManager, chatStreamManager;
146683
148002
  var init_chat = __esm({
146684
148003
  "../dashboard/src/chat.ts"() {
@@ -146712,7 +148031,7 @@ var init_chat = __esm({
146712
148031
  };
146713
148032
  RATE_LIMIT_WINDOW_MS5 = 60 * 1e3;
146714
148033
  MAX_REFERENCED_FILE_SIZE = 50 * 1024;
146715
- ChatStreamManager = class extends EventEmitter31 {
148034
+ ChatStreamManager = class extends EventEmitter32 {
146716
148035
  constructor(bufferSize = 100) {
146717
148036
  super();
146718
148037
  this.bufferSize = bufferSize;
@@ -151496,7 +152815,7 @@ var init_register_messaging_scripts = __esm({
151496
152815
  // ../dashboard/src/terminal.ts
151497
152816
  import { spawn as spawn13 } from "node:child_process";
151498
152817
  import { randomUUID as randomUUID25 } from "node:crypto";
151499
- import { EventEmitter as EventEmitter32 } from "node:events";
152818
+ import { EventEmitter as EventEmitter33 } from "node:events";
151500
152819
  function extractBaseCommand(command) {
151501
152820
  let trimmed = command.trim();
151502
152821
  while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed)) {
@@ -151642,7 +152961,7 @@ var init_terminal = __esm({
151642
152961
  // >(...) — process substitution (bash)
151643
152962
  ];
151644
152963
  CHAIN_OPERATORS = /&&|\|\||;|(?<!\|)\|(?!\|)/;
151645
- TerminalSessionManager = class extends EventEmitter32 {
152964
+ TerminalSessionManager = class extends EventEmitter33 {
151646
152965
  sessions = /* @__PURE__ */ new Map();
151647
152966
  defaultTimeout = 3e4;
151648
152967
  // 30 seconds
@@ -151850,19 +153169,19 @@ var init_register_agents_projects_nodes = __esm({
151850
153169
  });
151851
153170
 
151852
153171
  // ../dashboard/src/exec-file.ts
151853
- import { execFile as execFile18 } from "node:child_process";
151854
- import { promisify as promisify49 } from "node:util";
151855
- var execFileAsync13;
153172
+ import { execFile as execFile19 } from "node:child_process";
153173
+ import { promisify as promisify50 } from "node:util";
153174
+ var execFileAsync14;
151856
153175
  var init_exec_file = __esm({
151857
153176
  "../dashboard/src/exec-file.ts"() {
151858
153177
  "use strict";
151859
- execFileAsync13 = promisify49(execFile18);
153178
+ execFileAsync14 = promisify50(execFile19);
151860
153179
  }
151861
153180
  });
151862
153181
 
151863
153182
  // ../dashboard/src/routes/register-project-routes.ts
151864
153183
  import * as fsPromises from "node:fs/promises";
151865
- var access9, stat9, mkdir16, readdir9, rm7;
153184
+ var access9, stat9, mkdir16, readdir9, rm8;
151866
153185
  var init_register_project_routes = __esm({
151867
153186
  "../dashboard/src/routes/register-project-routes.ts"() {
151868
153187
  "use strict";
@@ -151875,7 +153194,7 @@ var init_register_project_routes = __esm({
151875
153194
  stat: stat9,
151876
153195
  mkdir: mkdir16,
151877
153196
  readdir: readdir9,
151878
- rm: rm7
153197
+ rm: rm8
151879
153198
  } = fsPromises);
151880
153199
  }
151881
153200
  });
@@ -152071,7 +153390,7 @@ var init_agent_generation = __esm({
152071
153390
 
152072
153391
  // ../dashboard/src/routes/register-agent-import-export-generation-routes.ts
152073
153392
  import * as fsPromises2 from "node:fs/promises";
152074
- var mkdtemp3, access10, stat10, mkdir17, rm8, fsWriteFile;
153393
+ var mkdtemp4, access10, stat10, mkdir17, rm9, fsWriteFile;
152075
153394
  var init_register_agent_import_export_generation_routes = __esm({
152076
153395
  "../dashboard/src/routes/register-agent-import-export-generation-routes.ts"() {
152077
153396
  "use strict";
@@ -152079,7 +153398,7 @@ var init_register_agent_import_export_generation_routes = __esm({
152079
153398
  init_ai_session_diagnostics();
152080
153399
  init_sse_buffer();
152081
153400
  init_agent_generation();
152082
- ({ 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);
152083
153402
  }
152084
153403
  });
152085
153404
 
@@ -153349,6 +154668,16 @@ var init_todo_routes = __esm({
153349
154668
  }
153350
154669
  });
153351
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
+
153352
154681
  // ../../plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js
153353
154682
  function compareOrderedEntities(a, b2) {
153354
154683
  if (a.orderIndex !== b2.orderIndex) {
@@ -153514,13 +154843,13 @@ var init_roadmap_ordering = __esm({
153514
154843
  });
153515
154844
 
153516
154845
  // ../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js
153517
- import { EventEmitter as EventEmitter33 } from "node:events";
154846
+ import { EventEmitter as EventEmitter34 } from "node:events";
153518
154847
  var RoadmapStore;
153519
154848
  var init_roadmap_store = __esm({
153520
154849
  "../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js"() {
153521
154850
  "use strict";
153522
154851
  init_roadmap_ordering();
153523
- RoadmapStore = class extends EventEmitter33 {
154852
+ RoadmapStore = class extends EventEmitter34 {
153524
154853
  db;
153525
154854
  /**
153526
154855
  * Creates a new RoadmapStore instance.
@@ -155024,7 +156353,7 @@ var init_src5 = __esm({
155024
156353
  });
155025
156354
 
155026
156355
  // ../dashboard/src/roadmap-routes.ts
155027
- import { Router as Router9 } from "express";
156356
+ import { Router as Router10 } from "express";
155028
156357
  var init_roadmap_routes3 = __esm({
155029
156358
  "../dashboard/src/roadmap-routes.ts"() {
155030
156359
  "use strict";
@@ -155068,7 +156397,7 @@ var init_dev_server_process = __esm({
155068
156397
  });
155069
156398
 
155070
156399
  // ../dashboard/src/dev-server-routes.ts
155071
- import { Router as Router10 } from "express";
156400
+ import { Router as Router11 } from "express";
155072
156401
  var init_dev_server_routes = __esm({
155073
156402
  "../dashboard/src/dev-server-routes.ts"() {
155074
156403
  "use strict";
@@ -155080,7 +156409,7 @@ var init_dev_server_routes = __esm({
155080
156409
  });
155081
156410
 
155082
156411
  // ../dashboard/src/routes/register-stash-recovery-routes.ts
155083
- import { Router as Router11 } from "express";
156412
+ import { Router as Router12 } from "express";
155084
156413
  var init_register_stash_recovery_routes = __esm({
155085
156414
  "../dashboard/src/routes/register-stash-recovery-routes.ts"() {
155086
156415
  "use strict";
@@ -155099,6 +156428,7 @@ var init_register_integrated_routers = __esm({
155099
156428
  init_research_routes();
155100
156429
  init_experiment_routes();
155101
156430
  init_todo_routes();
156431
+ init_goals_routes();
155102
156432
  init_roadmap_routes3();
155103
156433
  init_dev_server_routes();
155104
156434
  init_register_stash_recovery_routes();
@@ -157463,7 +158793,7 @@ var require_extension = __commonJS({
157463
158793
  var require_websocket = __commonJS({
157464
158794
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket.js"(exports, module) {
157465
158795
  "use strict";
157466
- var EventEmitter34 = __require("events");
158796
+ var EventEmitter35 = __require("events");
157467
158797
  var https = __require("https");
157468
158798
  var http = __require("http");
157469
158799
  var net2 = __require("net");
@@ -157495,7 +158825,7 @@ var require_websocket = __commonJS({
157495
158825
  var protocolVersions = [8, 13];
157496
158826
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
157497
158827
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
157498
- var WebSocket2 = class _WebSocket extends EventEmitter34 {
158828
+ var WebSocket2 = class _WebSocket extends EventEmitter35 {
157499
158829
  /**
157500
158830
  * Create a new `WebSocket`.
157501
158831
  *
@@ -158492,7 +159822,7 @@ var require_subprotocol = __commonJS({
158492
159822
  var require_websocket_server = __commonJS({
158493
159823
  "../../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports, module) {
158494
159824
  "use strict";
158495
- var EventEmitter34 = __require("events");
159825
+ var EventEmitter35 = __require("events");
158496
159826
  var http = __require("http");
158497
159827
  var { Duplex } = __require("stream");
158498
159828
  var { createHash: createHash15 } = __require("crypto");
@@ -158505,7 +159835,7 @@ var require_websocket_server = __commonJS({
158505
159835
  var RUNNING = 0;
158506
159836
  var CLOSING = 1;
158507
159837
  var CLOSED = 2;
158508
- var WebSocketServer2 = class extends EventEmitter34 {
159838
+ var WebSocketServer2 = class extends EventEmitter35 {
158509
159839
  /**
158510
159840
  * Create a `WebSocketServer` instance.
158511
159841
  *
@@ -158943,7 +160273,7 @@ var init_view_chunk_manifest = __esm({
158943
160273
 
158944
160274
  // ../dashboard/src/server.ts
158945
160275
  import express from "express";
158946
- import { join as join55, dirname as dirname21 } from "node:path";
160276
+ import { join as join56, dirname as dirname21 } from "node:path";
158947
160277
  import { fileURLToPath as fileURLToPath6 } from "node:url";
158948
160278
  function clearAiSessionCleanupInterval() {
158949
160279
  if (!aiSessionCleanupIntervalHandle) {
@@ -159052,6 +160382,7 @@ var init_src6 = __esm({
159052
160382
  init_rate_limit();
159053
160383
  init_github_poll();
159054
160384
  init_github_issue_comment();
160385
+ init_github_source_issue_close();
159055
160386
  init_github_tracking_comments();
159056
160387
  init_github_tracking_state();
159057
160388
  init_github_tracking_reconciler();
@@ -159248,7 +160579,7 @@ __export(task_exports, {
159248
160579
  });
159249
160580
  import { createInterface as createInterface2 } from "node:readline/promises";
159250
160581
  import { watchFile, unwatchFile, statSync as statSync7, existsSync as existsSync44, readFileSync as readFileSync15 } from "node:fs";
159251
- import { basename as basename16, join as join56 } from "node:path";
160582
+ import { basename as basename16, join as join57 } from "node:path";
159252
160583
  function formatGhErrorForCli(err) {
159253
160584
  const structured = classifyGhError(err);
159254
160585
  const lines = [`GitHub error: ${structured.message}`];
@@ -159709,7 +161040,7 @@ async function runTaskLogs(id, options = {}, projectName) {
159709
161040
  printEntries(filteredEntries);
159710
161041
  if (options.follow) {
159711
161042
  const projectPath = projectContext?.projectPath ?? process.cwd();
159712
- const logPath = join56(projectPath, ".fusion", "tasks", id, "agent.log");
161043
+ const logPath = join57(projectPath, ".fusion", "tasks", id, "agent.log");
159713
161044
  if (!existsSync44(logPath)) {
159714
161045
  console.log(`
159715
161046
  Waiting for log file to be created...`);
@@ -160887,8 +162218,8 @@ init_gh_cli();
160887
162218
  init_src2();
160888
162219
  init_src6();
160889
162220
  import { Type as Type8 } from "typebox";
160890
- import { StringEnum } from "@mariozechner/pi-ai";
160891
- import { resolve as resolve31, basename as basename17, extname as extname3, join as join57 } from "node:path";
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";
160892
162223
  import { readFile as readFile28 } from "node:fs/promises";
160893
162224
  import { existsSync as existsSync45 } from "node:fs";
160894
162225
  import { spawn as spawn16 } from "node:child_process";
@@ -160914,12 +162245,12 @@ function resolveProjectRoot2(cwd) {
160914
162245
  warnedMissingProjectRootResolver = true;
160915
162246
  console.warn("[fusion-extension] @fusion/core.getProjectRootFromWorktree is unavailable; using filesystem fallback resolution.");
160916
162247
  }
160917
- if (worktreeProjectRoot && existsSync45(join57(worktreeProjectRoot, ".fusion"))) {
162248
+ if (worktreeProjectRoot && existsSync45(join58(worktreeProjectRoot, ".fusion"))) {
160918
162249
  return worktreeProjectRoot;
160919
162250
  }
160920
162251
  let current = resolve31(cwd);
160921
162252
  while (true) {
160922
- if (existsSync45(join57(current, ".fusion"))) {
162253
+ if (existsSync45(join58(current, ".fusion"))) {
160923
162254
  return current;
160924
162255
  }
160925
162256
  const parent = resolve31(current, "..");
@@ -160940,7 +162271,7 @@ async function getStore2(cwd) {
160940
162271
  return store;
160941
162272
  }
160942
162273
  function getFusionDir(cwd) {
160943
- return join57(resolveProjectRoot2(cwd), ".fusion");
162274
+ return join58(resolveProjectRoot2(cwd), ".fusion");
160944
162275
  }
160945
162276
  function emitSecretAudit(store, ctx, mutationType, target, metadata) {
160946
162277
  if (!ctx.runId || !ctx.agentId) return;
@@ -162804,6 +164135,136 @@ Status: ${createdMission.status}${createdMission.autoAdvance ? "\nAuto-advance:
162804
164135
  };
162805
164136
  }
162806
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
+ });
162807
164268
  pi2.registerTool({
162808
164269
  name: "fn_mission_show",
162809
164270
  label: "fn: Show Mission",