@runfusion/fusion 0.27.0 → 0.27.1

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 (52) hide show
  1. package/dist/bin.js +1865 -576
  2. package/dist/client/assets/{AgentDetailView-DwLmRXTY.js → AgentDetailView-shgiiUb4.js} +1 -1
  3. package/dist/client/assets/{AgentsView-CV3vm7Qk.css → AgentsView-B3ADnF0D.css} +1 -1
  4. package/dist/client/assets/{AgentsView-D-N6aA0P.js → AgentsView-CpwqOVDz.js} +8 -8
  5. package/dist/client/assets/ChatView-DyRBOIKL.js +1 -0
  6. package/dist/client/assets/{DevServerView-BiA1nYtt.js → DevServerView-Cdelj9-m.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-DvBviDG6.js → DirectoryPicker-C0kmRv0u.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-BWXOxpuq.js → DocumentsView-B94U9ijs.js} +1 -1
  9. package/dist/client/assets/{EvalsView-CJFbtL7i.js → EvalsView-O_4YWy--.js} +1 -1
  10. package/dist/client/assets/{ExperimentalAgentOnboardingModal-DuGIPd0B.js → ExperimentalAgentOnboardingModal-CkEiF85-.js} +1 -1
  11. package/dist/client/assets/{InsightsView-BBpRiolN.js → InsightsView-D-Qe0tRr.js} +1 -1
  12. package/dist/client/assets/{MemoryView-48LuNkKk.js → MemoryView-CoRUmRvb.js} +1 -1
  13. package/dist/client/assets/{NodesView-CGQWSNZM.js → NodesView-DQzXjcLc.js} +1 -1
  14. package/dist/client/assets/{PiExtensionsManager-i-7UL2oh.js → PiExtensionsManager-Dn1LmFbq.js} +1 -1
  15. package/dist/client/assets/{PluginManager-DoSAykD6.js → PluginManager-Y0fs-6No.js} +1 -1
  16. package/dist/client/assets/{ResearchView-XZuRtOxE.js → ResearchView-CjOxKhdS.js} +1 -1
  17. package/dist/client/assets/{SettingsModal-CmeF8CN4.js → SettingsModal-Bg1-3JO_.js} +1 -1
  18. package/dist/client/assets/SettingsModal-DL7tjJQa.js +31 -0
  19. package/dist/client/assets/{SetupWizardModal-CgtvpMX9.js → SetupWizardModal-DuzYPbuJ.js} +1 -1
  20. package/dist/client/assets/{SkillsView-DErYRumF.js → SkillsView-BIFoVNUf.js} +1 -1
  21. package/dist/client/assets/{StashRecoveryView-QJrNS4Vg.js → StashRecoveryView-C52KsV7f.js} +1 -1
  22. package/dist/client/assets/{TodoView-BD9NRwq0.js → TodoView-sS_mT0Y7.js} +1 -1
  23. package/dist/client/assets/{dashboard-view-Ws9_ZnKu.js → dashboard-view-MB-86hAu.js} +1 -1
  24. package/dist/client/assets/{folder-open-CHSlllzf.js → folder-open-B9cwJ-OX.js} +1 -1
  25. package/dist/client/assets/{index-bEwSVl7B.js → index-BOjPRqEk.js} +161 -161
  26. package/dist/client/assets/index-BmSEq8Rb.css +1 -0
  27. package/dist/client/assets/{star-BgVwWAPz.js → star-BDn04UYV.js} +1 -1
  28. package/dist/client/assets/{upload-CAzycxr9.js → upload-zdPPycKQ.js} +1 -1
  29. package/dist/client/assets/{users-CZnxCCCJ.js → users-CPYZjK2g.js} +1 -1
  30. package/dist/client/index.html +2 -2
  31. package/dist/client/version.json +1 -1
  32. package/dist/droid-cli/package.json +1 -1
  33. package/dist/extension.js +1721 -532
  34. package/dist/pi-claude-cli/package.json +1 -1
  35. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  36. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  37. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  38. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  39. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  40. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  41. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  42. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  43. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  44. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  45. package/package.json +1 -1
  46. package/skill/fusion/SKILL.md +1 -1
  47. package/skill/fusion/references/engine-tools.md +1 -1
  48. package/skill/fusion/references/extension-tools.md +1 -0
  49. package/skill/fusion/workflows/task-management.md +3 -1
  50. package/dist/client/assets/ChatView-DnCdKu8Z.js +0 -1
  51. package/dist/client/assets/SettingsModal-DBcjf9Bu.js +0 -31
  52. package/dist/client/assets/index-DCovGm5b.css +0 -1
package/dist/bin.js CHANGED
@@ -66,6 +66,7 @@ var init_settings_schema = __esm({
66
66
  ntfyEnabled: false,
67
67
  ntfyTopic: void 0,
68
68
  ntfyBaseUrl: void 0,
69
+ ntfyAccessToken: void 0,
69
70
  ntfyEvents: [
70
71
  "in-review",
71
72
  "merged",
@@ -249,6 +250,7 @@ var init_settings_schema = __esm({
249
250
  worktreeRebaseLocalBase: true,
250
251
  mergeConflictStrategy: "smart-prefer-main",
251
252
  workflowStepTimeoutMs: 36e4,
253
+ workflowRevisionForkOnScopeMismatch: true,
252
254
  strictScopeEnforcement: false,
253
255
  buildRetryCount: 0,
254
256
  verificationFixRetries: 3,
@@ -3359,9 +3361,9 @@ var init_sqlite_adapter = __esm({
3359
3361
 
3360
3362
  // ../core/src/db.ts
3361
3363
  import { isAbsolute, join as join2 } from "node:path";
3362
- import { mkdirSync, existsSync } from "node:fs";
3364
+ import { mkdirSync, existsSync, statSync } from "node:fs";
3363
3365
  import { spawnSync } from "node:child_process";
3364
- import { randomUUID } from "node:crypto";
3366
+ import { createHash as createHash2, randomUUID } from "node:crypto";
3365
3367
  function toJson(value) {
3366
3368
  if (value === void 0 || value === null) return "[]";
3367
3369
  if (Array.isArray(value) && value.length === 0) return "[]";
@@ -3381,6 +3383,15 @@ function fromJson(json) {
3381
3383
  return void 0;
3382
3384
  }
3383
3385
  }
3386
+ function isSqliteLockError(error) {
3387
+ const message = error instanceof Error ? error.message : String(error);
3388
+ return /SQLITE_(?:BUSY|LOCKED)|database is locked|database table is locked/i.test(message);
3389
+ }
3390
+ function sleepSync(ms) {
3391
+ if (ms <= 0) return;
3392
+ const signal = new Int32Array(new SharedArrayBuffer(4));
3393
+ Atomics.wait(signal, 0, 0, ms);
3394
+ }
3384
3395
  function probeFts5(db) {
3385
3396
  if (process.env.FUSION_DISABLE_FTS5 === "1" || process.env.FUSION_DISABLE_FTS5 === "true") {
3386
3397
  return false;
@@ -3480,15 +3491,28 @@ function getSchemaCompatibilityTableSchemas() {
3480
3491
  }
3481
3492
  return tables;
3482
3493
  }
3494
+ function canonicalizeSchemaTables(tables) {
3495
+ return Object.fromEntries(
3496
+ [...tables.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([tableName, columns]) => [
3497
+ tableName,
3498
+ Object.fromEntries(
3499
+ [...columns.entries()].sort(([left], [right]) => left.localeCompare(right))
3500
+ )
3501
+ ])
3502
+ );
3503
+ }
3483
3504
  function createDatabase(fusionDir, options) {
3484
3505
  return new Database(fusionDir, options);
3485
3506
  }
3486
- var SCHEMA_VERSION, SCHEMA_SQL, TABLE_LEVEL_CONSTRAINT_PREFIXES, SCHEMA_TABLE_SCHEMAS, MIGRATION_ONLY_TABLE_SCHEMAS, Database;
3507
+ var DEFAULT_SQLITE_BUSY_TIMEOUT_MS, DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS, DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS, SCHEMA_VERSION, SCHEMA_SQL, TABLE_LEVEL_CONSTRAINT_PREFIXES, SCHEMA_TABLE_SCHEMAS, MIGRATION_ONLY_TABLE_SCHEMAS, SCHEMA_COMPAT_FINGERPRINT, Database;
3487
3508
  var init_db = __esm({
3488
3509
  "../core/src/db.ts"() {
3489
3510
  "use strict";
3490
3511
  init_sqlite_adapter();
3491
3512
  init_types();
3513
+ DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
3514
+ DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
3515
+ DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
3492
3516
  SCHEMA_VERSION = 72;
3493
3517
  SCHEMA_SQL = `
3494
3518
  -- Tasks table with JSON columns for nested data
@@ -3580,6 +3604,9 @@ CREATE TABLE IF NOT EXISTS tasks (
3580
3604
  );
3581
3605
 
3582
3606
  -- Config table (single row with project settings)
3607
+ -- nextId is a deprecated legacy allocator counter retained read-only for one
3608
+ -- release so older databases/config consumers can still load it during the
3609
+ -- distributed_task_id_state transition.
3583
3610
  CREATE TABLE IF NOT EXISTS config (
3584
3611
  id INTEGER PRIMARY KEY CHECK (id = 1),
3585
3612
  nextId INTEGER DEFAULT 1,
@@ -4354,6 +4381,20 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
4354
4381
  createdAt: "TEXT NOT NULL"
4355
4382
  }
4356
4383
  };
4384
+ SCHEMA_COMPAT_FINGERPRINT = createHash2("sha1").update(
4385
+ JSON.stringify({
4386
+ schemaVersion: SCHEMA_VERSION,
4387
+ schemaSqlTables: canonicalizeSchemaTables(SCHEMA_TABLE_SCHEMAS),
4388
+ migrationOnlyTableSchemas: Object.fromEntries(
4389
+ Object.entries(MIGRATION_ONLY_TABLE_SCHEMAS).sort(([left], [right]) => left.localeCompare(right)).map(([tableName, columns]) => [
4390
+ tableName,
4391
+ Object.fromEntries(
4392
+ Object.entries(columns).sort(([left], [right]) => left.localeCompare(right))
4393
+ )
4394
+ ])
4395
+ )
4396
+ })
4397
+ ).digest("hex");
4357
4398
  Database = class _Database {
4358
4399
  static sharedIntegrityChecks = /* @__PURE__ */ new Map();
4359
4400
  db;
@@ -4371,10 +4412,16 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
4371
4412
  _fts5Available;
4372
4413
  integrityCheckScheduled = false;
4373
4414
  closed = false;
4415
+ busyTimeoutMs;
4416
+ lockRecoveryWindowMs;
4417
+ lockRecoveryDelayMs;
4374
4418
  constructor(fusionDir, options) {
4375
4419
  const inMemory = options?.inMemory === true;
4376
4420
  this.inMemory = inMemory;
4377
4421
  this.dbPath = inMemory ? ":memory:" : join2(fusionDir, "fusion.db");
4422
+ this.busyTimeoutMs = Math.max(0, options?.busyTimeoutMs ?? DEFAULT_SQLITE_BUSY_TIMEOUT_MS);
4423
+ this.lockRecoveryWindowMs = Math.max(0, options?.lockRecoveryWindowMs ?? DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS);
4424
+ this.lockRecoveryDelayMs = Math.max(1, options?.lockRecoveryDelayMs ?? DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS);
4378
4425
  if (!inMemory && !isAbsolute(fusionDir)) {
4379
4426
  throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
4380
4427
  }
@@ -4394,13 +4441,13 @@ This means a caller passed a .fusion directory where a project root was expected
4394
4441
  throw new Error(`Failed to open Fusion database at ${this.dbPath}: ${message}`);
4395
4442
  }
4396
4443
  if (!inMemory) {
4397
- this.db.exec("PRAGMA busy_timeout = 5000");
4444
+ this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
4398
4445
  this.db.exec("PRAGMA journal_mode = WAL");
4399
4446
  this.db.exec("PRAGMA synchronous = NORMAL");
4400
4447
  this.db.exec("PRAGMA wal_autocheckpoint = 100");
4401
4448
  this.db.exec("PRAGMA journal_size_limit = 4194304");
4402
4449
  } else {
4403
- this.db.exec("PRAGMA busy_timeout = 5000");
4450
+ this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
4404
4451
  }
4405
4452
  this.db.exec("PRAGMA foreign_keys = ON");
4406
4453
  this._fts5Available = probeFts5(this.db);
@@ -4511,6 +4558,44 @@ This means a caller passed a .fusion directory where a project root was expected
4511
4558
  });
4512
4559
  return rebuilt.status === 0;
4513
4560
  }
4561
+ /**
4562
+ * Run WAL truncation + VACUUM and report compaction stats.
4563
+ *
4564
+ * In-memory databases no-op and return zeroed stats. Disk-backed databases
4565
+ * sample file size before/after compaction, run `wal_checkpoint(TRUNCATE)`,
4566
+ * and then run `VACUUM` while the connection is in EXCLUSIVE locking mode to
4567
+ * prevent concurrent writes from other connections during maintenance.
4568
+ */
4569
+ vacuum() {
4570
+ if (this.inMemory) {
4571
+ return { beforeBytes: 0, afterBytes: 0, durationMs: 0 };
4572
+ }
4573
+ const beforeBytes = existsSync(this.dbPath) ? statSync(this.dbPath).size : 0;
4574
+ const startedAt = Date.now();
4575
+ this.db.exec("PRAGMA locking_mode=EXCLUSIVE");
4576
+ try {
4577
+ try {
4578
+ this.walCheckpoint("TRUNCATE");
4579
+ } catch (error) {
4580
+ const message = error instanceof Error ? error.message : String(error);
4581
+ throw new Error(`Database vacuum maintenance failed during WAL checkpoint (dbPath=${this.dbPath}): ${message}`);
4582
+ }
4583
+ try {
4584
+ this.db.exec("VACUUM");
4585
+ } catch (error) {
4586
+ const message = error instanceof Error ? error.message : String(error);
4587
+ throw new Error(`Database vacuum maintenance failed during VACUUM (dbPath=${this.dbPath}): ${message}`);
4588
+ }
4589
+ const afterBytes = existsSync(this.dbPath) ? statSync(this.dbPath).size : 0;
4590
+ return {
4591
+ beforeBytes,
4592
+ afterBytes,
4593
+ durationMs: Date.now() - startedAt
4594
+ };
4595
+ } finally {
4596
+ this.db.exec("PRAGMA locking_mode=NORMAL");
4597
+ }
4598
+ }
4514
4599
  /**
4515
4600
  * Initialize the database: create tables if they don't exist
4516
4601
  * and seed meta values.
@@ -4525,10 +4610,20 @@ This means a caller passed a .fusion directory where a project root was expected
4525
4610
  `INSERT OR IGNORE INTO __meta (key, value) VALUES ('lastModified', '${Date.now()}')`
4526
4611
  );
4527
4612
  this.migrate();
4528
- this.ensureSchemaCompatibility();
4529
- this.ensureRoutinesSchemaCompatibility();
4530
- this.ensureInsightRunsSchemaCompatibility();
4531
- this.ensureEvalTaskResultsSchemaCompatibility();
4613
+ const schemaCompatFingerprint = this.getMetaValue("schemaCompatFingerprint");
4614
+ const skipColumnReconciliation = schemaCompatFingerprint === SCHEMA_COMPAT_FINGERPRINT;
4615
+ const tableColumnsCache = skipColumnReconciliation ? void 0 : /* @__PURE__ */ new Map();
4616
+ const compatibilityOptions = {
4617
+ tableColumnsCache,
4618
+ skipColumnReconciliation
4619
+ };
4620
+ this.ensureSchemaCompatibility(compatibilityOptions);
4621
+ this.ensureRoutinesSchemaCompatibility(compatibilityOptions);
4622
+ this.ensureInsightRunsSchemaCompatibility(compatibilityOptions);
4623
+ this.ensureEvalTaskResultsSchemaCompatibility(compatibilityOptions);
4624
+ if (!skipColumnReconciliation) {
4625
+ this.setMetaValue("schemaCompatFingerprint", SCHEMA_COMPAT_FINGERPRINT);
4626
+ }
4532
4627
  const configNow = (/* @__PURE__ */ new Date()).toISOString();
4533
4628
  this.db.exec(
4534
4629
  `INSERT OR IGNORE INTO config (id, nextId, nextWorkflowStepId, settings, workflowSteps, updatedAt) VALUES (1, 1, 1, '${JSON.stringify(DEFAULT_PROJECT_SETTINGS)}', '[]', '${configNow}')`
@@ -4546,20 +4641,27 @@ This means a caller passed a .fusion directory where a project root was expected
4546
4641
  * re-run even if a previous migration partially applied.
4547
4642
  */
4548
4643
  /**
4549
- * Applies unconditional column reconciliation for all known project DB tables.
4644
+ * Reconciles additive columns for every known project DB table unless the
4645
+ * persisted `schemaCompatFingerprint` already matches SCHEMA_COMPAT_FINGERPRINT.
4550
4646
  *
4551
- * FN-3879 introduced a tasks checkout-column self-heal, FN-3898 formalized it,
4552
- * and FN-3887 generalized the guardrail so migration-version drift no longer
4553
- * determines whether additive columns exist. Invariant: every column declared
4554
- * in SCHEMA_SQL or MIGRATION_ONLY_TABLE_SCHEMAS exists on any live table after
4555
- * this method returns, regardless of the persisted schemaVersion.
4647
+ * The fingerprint is invalidated automatically by SCHEMA_VERSION changes and by
4648
+ * edits to the canonicalized column declarations from SCHEMA_SQL or
4649
+ * MIGRATION_ONLY_TABLE_SCHEMAS. When it is absent or stale, this method runs the
4650
+ * full FN-3879/FN-3887/FN-3898 safety pass so every declared column exists on
4651
+ * every live table after init() returns.
4556
4652
  */
4557
- ensureSchemaCompatibility() {
4653
+ ensureSchemaCompatibility(options = {}) {
4654
+ if (options.skipColumnReconciliation) {
4655
+ return;
4656
+ }
4558
4657
  const knownTableSchemas = getSchemaCompatibilityTableSchemas();
4658
+ const tableColumnsCache = options.tableColumnsCache;
4559
4659
  for (const [tableName, columns] of knownTableSchemas) {
4560
4660
  if (!this.hasTable(tableName)) continue;
4661
+ const cachedColumns = this.getTableColumns(tableName, true, tableColumnsCache);
4561
4662
  for (const [columnName, columnDefinition] of columns) {
4562
- this.addColumnIfMissing(tableName, columnName, columnDefinition);
4663
+ if (cachedColumns.has(columnName)) continue;
4664
+ this.addColumnIfMissingCached(tableName, columnName, columnDefinition, tableColumnsCache);
4563
4665
  }
4564
4666
  }
4565
4667
  }
@@ -4570,10 +4672,14 @@ This means a caller passed a .fusion directory where a project root was expected
4570
4672
  * agent IDs from earlier table definitions. `RoutineStore.rowToRoutine()` and
4571
4673
  * backup routine sync expect a safe string value, so normalize to ''.
4572
4674
  */
4573
- ensureRoutinesSchemaCompatibility() {
4675
+ ensureRoutinesSchemaCompatibility(options = {}) {
4574
4676
  if (!this.hasTable("routines")) {
4575
4677
  return;
4576
4678
  }
4679
+ if (!options.skipColumnReconciliation) {
4680
+ this.addColumnIfMissingCached("routines", "agentId", "TEXT DEFAULT ''", options.tableColumnsCache);
4681
+ this.addColumnIfMissingCached("routines", "scope", "TEXT DEFAULT 'project'", options.tableColumnsCache);
4682
+ }
4577
4683
  this.db.exec("UPDATE routines SET agentId = '' WHERE agentId IS NULL");
4578
4684
  this.db.exec("UPDATE routines SET scope = 'project' WHERE scope IS NULL OR TRIM(scope) = ''");
4579
4685
  this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesNextRunAt ON routines(nextRunAt)");
@@ -4587,13 +4693,17 @@ This means a caller passed a .fusion directory where a project root was expected
4587
4693
  * remains focused on index creation that should run after the generic column
4588
4694
  * backfill pass.
4589
4695
  */
4590
- ensureInsightRunsSchemaCompatibility() {
4696
+ ensureInsightRunsSchemaCompatibility(options = {}) {
4591
4697
  if (!this.hasTable("project_insight_runs")) {
4592
4698
  return;
4593
4699
  }
4700
+ if (!options.skipColumnReconciliation) {
4701
+ this.addColumnIfMissingCached("project_insight_runs", "lifecycle", "TEXT", options.tableColumnsCache);
4702
+ this.addColumnIfMissingCached("project_insight_runs", "cancelledAt", "TEXT", options.tableColumnsCache);
4703
+ }
4594
4704
  this.db.exec(`CREATE INDEX IF NOT EXISTS idxInsightRunsProjectTriggerStatus ON project_insight_runs(projectId, trigger, status)`);
4595
4705
  }
4596
- ensureEvalTaskResultsSchemaCompatibility() {
4706
+ ensureEvalTaskResultsSchemaCompatibility(_options = {}) {
4597
4707
  if (!this.hasTable("eval_task_results")) {
4598
4708
  return;
4599
4709
  }
@@ -5935,12 +6045,26 @@ This means a caller passed a .fusion directory where a project root was expected
5935
6045
  const lower = message.toLowerCase();
5936
6046
  return lower.includes("corruption found reading blob") || lower.includes("database disk image is malformed") || lower.includes("fts5") && lower.includes("corrupt");
5937
6047
  }
6048
+ /**
6049
+ * Read the declared columns for a table.
6050
+ */
6051
+ getTableColumns(table, useCache = false, cache) {
6052
+ if (useCache && cache?.has(table)) {
6053
+ return cache.get(table) ?? /* @__PURE__ */ new Set();
6054
+ }
6055
+ const columns = new Set(
6056
+ this.db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name)
6057
+ );
6058
+ if (useCache && cache) {
6059
+ cache.set(table, columns);
6060
+ }
6061
+ return columns;
6062
+ }
5938
6063
  /**
5939
6064
  * Check whether a table has a given column.
5940
6065
  */
5941
6066
  hasColumn(table, column) {
5942
- const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
5943
- return cols.some((c) => c.name === column);
6067
+ return this.getTableColumns(table).has(column);
5944
6068
  }
5945
6069
  /**
5946
6070
  * Add a column to a table if it does not already exist.
@@ -5950,6 +6074,20 @@ This means a caller passed a .fusion directory where a project root was expected
5950
6074
  this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
5951
6075
  }
5952
6076
  }
6077
+ /**
6078
+ * Add a column using a per-init table-info cache when available.
6079
+ */
6080
+ addColumnIfMissingCached(table, column, definition, cache) {
6081
+ const columns = this.getTableColumns(table, Boolean(cache), cache);
6082
+ if (columns.has(column)) {
6083
+ return;
6084
+ }
6085
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
6086
+ columns.add(column);
6087
+ if (cache) {
6088
+ cache.set(table, columns);
6089
+ }
6090
+ }
5953
6091
  /**
5954
6092
  * Normalize legacy steering comments into the unified comments field exactly once.
5955
6093
  *
@@ -6050,25 +6188,67 @@ This means a caller passed a .fusion directory where a project root was expected
6050
6188
  this.integrityCheckPending = false;
6051
6189
  this.db.close();
6052
6190
  }
6191
+ runWithLockRecovery(action, fn) {
6192
+ const deadline = Date.now() + this.lockRecoveryWindowMs;
6193
+ let attempt = 0;
6194
+ while (true) {
6195
+ try {
6196
+ fn();
6197
+ return;
6198
+ } catch (error) {
6199
+ if (!isSqliteLockError(error)) {
6200
+ throw error;
6201
+ }
6202
+ if (Date.now() >= deadline) {
6203
+ throw new Error(
6204
+ `SQLite ${action} failed after ${attempt + 1} attempt${attempt === 0 ? "" : "s"}: ${error instanceof Error ? error.message : String(error)}`
6205
+ );
6206
+ }
6207
+ const remainingMs = Math.max(0, deadline - Date.now());
6208
+ const delayMs = Math.min(this.lockRecoveryDelayMs * Math.max(1, attempt + 1), remainingMs);
6209
+ sleepSync(delayMs);
6210
+ attempt += 1;
6211
+ }
6212
+ }
6213
+ }
6053
6214
  /**
6054
6215
  * Execute a function inside a SQLite transaction.
6055
6216
  * Supports nested calls via SAVEPOINTs.
6056
6217
  * If the function throws, the transaction/savepoint is rolled back.
6057
6218
  * If the function returns normally, the transaction/savepoint is committed.
6219
+ *
6220
+ * Outermost transactions default to `BEGIN` (DEFERRED) so read-only callers
6221
+ * avoid taking a writer lock until they actually mutate state.
6222
+ * Use `transactionImmediate()` for write-heavy paths that should acquire the
6223
+ * RESERVED lock before user code runs and fail/retry before the callback executes.
6058
6224
  */
6059
- transaction(fn) {
6225
+ transaction(fn, options) {
6060
6226
  const depth = this.transactionDepth++;
6061
6227
  const isOutermost = depth === 0;
6062
6228
  const savepointName = `sp_${depth}`;
6063
- if (isOutermost) {
6064
- this.db.exec("BEGIN");
6065
- } else {
6066
- this.db.exec(`SAVEPOINT ${savepointName}`);
6229
+ const mode = options?.mode ?? "deferred";
6230
+ try {
6231
+ if (isOutermost) {
6232
+ if (mode === "immediate") {
6233
+ this.runWithLockRecovery("BEGIN IMMEDIATE", () => {
6234
+ this.db.exec("BEGIN IMMEDIATE");
6235
+ });
6236
+ } else {
6237
+ this.db.exec("BEGIN");
6238
+ }
6239
+ } else {
6240
+ this.db.exec(`SAVEPOINT ${savepointName}`);
6241
+ }
6242
+ } catch (error) {
6243
+ this.transactionDepth--;
6244
+ throw error;
6067
6245
  }
6068
6246
  try {
6069
6247
  const result = fn();
6070
6248
  if (isOutermost) {
6071
- this.db.exec("COMMIT");
6249
+ this.runWithLockRecovery("COMMIT", () => {
6250
+ this.db.exec("COMMIT");
6251
+ });
6072
6252
  } else {
6073
6253
  this.db.exec(`RELEASE ${savepointName}`);
6074
6254
  }
@@ -6085,6 +6265,9 @@ This means a caller passed a .fusion directory where a project root was expected
6085
6265
  this.transactionDepth--;
6086
6266
  }
6087
6267
  }
6268
+ transactionImmediate(fn) {
6269
+ return this.transaction(fn, { mode: "immediate" });
6270
+ }
6088
6271
  /**
6089
6272
  * Execute plugin-provided schema initialization hooks.
6090
6273
  *
@@ -6120,14 +6303,24 @@ This means a caller passed a .fusion directory where a project root was expected
6120
6303
  exec(sql) {
6121
6304
  this.db.exec(sql);
6122
6305
  }
6306
+ getMetaValue(key) {
6307
+ const row = this.db.prepare("SELECT value FROM __meta WHERE key = ?").get(key);
6308
+ return row?.value;
6309
+ }
6310
+ /**
6311
+ * Persist a __meta value idempotently.
6312
+ */
6313
+ setMetaValue(key, value) {
6314
+ this.db.prepare("INSERT OR REPLACE INTO __meta (key, value) VALUES (?, ?)").run(key, value);
6315
+ }
6123
6316
  /**
6124
6317
  * Get the last modification timestamp (epoch ms).
6125
6318
  * Returns 0 if the value is not set.
6126
6319
  */
6127
6320
  getLastModified() {
6128
- const row = this.db.prepare("SELECT value FROM __meta WHERE key = 'lastModified'").get();
6129
- if (!row) return 0;
6130
- return parseInt(row.value, 10) || 0;
6321
+ const value = this.getMetaValue("lastModified");
6322
+ if (!value) return 0;
6323
+ return parseInt(value, 10) || 0;
6131
6324
  }
6132
6325
  /**
6133
6326
  * Update the last modification timestamp to the current time.
@@ -6146,9 +6339,9 @@ This means a caller passed a .fusion directory where a project root was expected
6146
6339
  * Get the schema version number.
6147
6340
  */
6148
6341
  getSchemaVersion() {
6149
- const row = this.db.prepare("SELECT value FROM __meta WHERE key = 'schemaVersion'").get();
6150
- if (!row) return 0;
6151
- return parseInt(row.value, 10) || 0;
6342
+ const value = this.getMetaValue("schemaVersion");
6343
+ if (!value) return 0;
6344
+ return parseInt(value, 10) || 0;
6152
6345
  }
6153
6346
  /**
6154
6347
  * Get the database file path.
@@ -6164,7 +6357,7 @@ This means a caller passed a .fusion directory where a project root was expected
6164
6357
  import { mkdir, readFile, writeFile, readdir, unlink, rename, access, appendFile } from "node:fs/promises";
6165
6358
  import { constants as fsConstants } from "node:fs";
6166
6359
  import { basename, dirname, join as join3, resolve as resolve2 } from "node:path";
6167
- import { randomUUID as randomUUID2, randomBytes, createHash as createHash2 } from "node:crypto";
6360
+ import { randomUUID as randomUUID2, randomBytes, createHash as createHash3 } from "node:crypto";
6168
6361
  import { EventEmitter } from "node:events";
6169
6362
  function resolveCreationRuntimeConfig(incoming, metadata) {
6170
6363
  const isEphemeral = isEphemeralAgent({ metadata });
@@ -7330,7 +7523,7 @@ var init_agent_store = __esm({
7330
7523
  throw new Error(`Agent ${agentId} not found`);
7331
7524
  }
7332
7525
  const token = randomBytes(32).toString("hex");
7333
- const tokenHash = createHash2("sha256").update(token).digest("hex");
7526
+ const tokenHash = createHash3("sha256").update(token).digest("hex");
7334
7527
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7335
7528
  const label = options?.label?.trim();
7336
7529
  const key = {
@@ -9486,10 +9679,10 @@ END;
9486
9679
  mkdirSync3(fusionDir, { recursive: true });
9487
9680
  }
9488
9681
  this.db = new DatabaseSync(inMemory ? ":memory:" : join6(fusionDir, "archive.db"));
9682
+ this.db.exec("PRAGMA busy_timeout = 5000");
9489
9683
  if (!inMemory) {
9490
9684
  this.db.exec("PRAGMA journal_mode = WAL");
9491
9685
  }
9492
- this.db.exec("PRAGMA busy_timeout = 5000");
9493
9686
  this._fts5Available = probeFts5(this.db);
9494
9687
  }
9495
9688
  /** True when this SQLite build has FTS5. See db.ts#probeFts5. */
@@ -13164,9 +13357,15 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13164
13357
  globalDir;
13165
13358
  /** Tracks transaction nesting depth for savepoint-based nested transactions. */
13166
13359
  transactionDepth = 0;
13167
- constructor(globalDir) {
13360
+ busyTimeoutMs;
13361
+ lockRecoveryWindowMs;
13362
+ lockRecoveryDelayMs;
13363
+ constructor(globalDir, options) {
13168
13364
  this.globalDir = resolveGlobalDir(globalDir);
13169
13365
  this.dbPath = join8(this.globalDir, "fusion-central.db");
13366
+ this.busyTimeoutMs = Math.max(0, options?.busyTimeoutMs ?? 5e3);
13367
+ this.lockRecoveryWindowMs = Math.max(0, options?.lockRecoveryWindowMs ?? 1e3);
13368
+ this.lockRecoveryDelayMs = Math.max(1, options?.lockRecoveryDelayMs ?? 50);
13170
13369
  if (!existsSync6(this.globalDir)) {
13171
13370
  mkdirSync4(this.globalDir, { recursive: true });
13172
13371
  }
@@ -13176,8 +13375,8 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13176
13375
  const message = error instanceof Error ? error.message : String(error);
13177
13376
  throw new Error(`Failed to open Fusion central database at ${this.dbPath}: ${message}`);
13178
13377
  }
13378
+ this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
13179
13379
  this.db.exec("PRAGMA journal_mode = WAL");
13180
- this.db.exec("PRAGMA busy_timeout = 5000");
13181
13380
  this.db.exec("PRAGMA foreign_keys = ON");
13182
13381
  }
13183
13382
  /**
@@ -13283,6 +13482,29 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13283
13482
  close() {
13284
13483
  this.db.close();
13285
13484
  }
13485
+ runWithLockRecovery(action, fn) {
13486
+ const deadline = Date.now() + this.lockRecoveryWindowMs;
13487
+ let attempt = 0;
13488
+ while (true) {
13489
+ try {
13490
+ fn();
13491
+ return;
13492
+ } catch (error) {
13493
+ if (!isSqliteLockError(error)) {
13494
+ throw error;
13495
+ }
13496
+ if (Date.now() >= deadline) {
13497
+ throw new Error(
13498
+ `SQLite ${action} failed after ${attempt + 1} attempt${attempt === 0 ? "" : "s"}: ${error instanceof Error ? error.message : String(error)}`
13499
+ );
13500
+ }
13501
+ const remainingMs = Math.max(0, deadline - Date.now());
13502
+ const delayMs = Math.min(this.lockRecoveryDelayMs * Math.max(1, attempt + 1), remainingMs);
13503
+ sleepSync(delayMs);
13504
+ attempt += 1;
13505
+ }
13506
+ }
13507
+ }
13286
13508
  /**
13287
13509
  * Execute a function inside a SQLite transaction.
13288
13510
  * Supports nested calls via SAVEPOINTs.
@@ -13293,15 +13515,24 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13293
13515
  const depth = this.transactionDepth++;
13294
13516
  const isOutermost = depth === 0;
13295
13517
  const savepointName = `sp_${depth}`;
13296
- if (isOutermost) {
13297
- this.db.exec("BEGIN");
13298
- } else {
13299
- this.db.exec(`SAVEPOINT ${savepointName}`);
13518
+ try {
13519
+ if (isOutermost) {
13520
+ this.runWithLockRecovery("BEGIN IMMEDIATE", () => {
13521
+ this.db.exec("BEGIN IMMEDIATE");
13522
+ });
13523
+ } else {
13524
+ this.db.exec(`SAVEPOINT ${savepointName}`);
13525
+ }
13526
+ } catch (error) {
13527
+ this.transactionDepth--;
13528
+ throw error;
13300
13529
  }
13301
13530
  try {
13302
13531
  const result = fn();
13303
13532
  if (isOutermost) {
13304
- this.db.exec("COMMIT");
13533
+ this.runWithLockRecovery("COMMIT", () => {
13534
+ this.db.exec("COMMIT");
13535
+ });
13305
13536
  } else {
13306
13537
  this.db.exec(`RELEASE ${savepointName}`);
13307
13538
  }
@@ -19922,8 +20153,8 @@ var init_system_metrics = __esm({
19922
20153
 
19923
20154
  // ../core/src/central-core.ts
19924
20155
  import { EventEmitter as EventEmitter11 } from "node:events";
19925
- import { createHash as createHash3, randomUUID as randomUUID9 } from "node:crypto";
19926
- import { existsSync as existsSync7, statSync } from "node:fs";
20156
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "node:crypto";
20157
+ import { existsSync as existsSync7, statSync as statSync2 } from "node:fs";
19927
20158
  import { mkdir as mkdir4 } from "node:fs/promises";
19928
20159
  import { isAbsolute as isAbsolute2, join as join11, basename as basename2, resolve as resolve5 } from "node:path";
19929
20160
  var CentralCore;
@@ -20032,7 +20263,7 @@ var init_central_core = __esm({
20032
20263
  if (!existsSync7(input.path)) {
20033
20264
  throw new Error(`Project path does not exist: ${input.path}`);
20034
20265
  }
20035
- if (!statSync(input.path).isDirectory()) {
20266
+ if (!statSync2(input.path).isDirectory()) {
20036
20267
  throw new Error(`Project path must be a directory: ${input.path}`);
20037
20268
  }
20038
20269
  const existingByPath = await this.getProjectByPath(input.path);
@@ -21771,7 +22002,7 @@ var init_central_core = __esm({
21771
22002
  const dbPath = this.db.getPath();
21772
22003
  let dbSizeBytes = 0;
21773
22004
  try {
21774
- dbSizeBytes = statSync(dbPath).size;
22005
+ dbSizeBytes = statSync2(dbPath).size;
21775
22006
  } catch {
21776
22007
  }
21777
22008
  return { projectCount, totalTasksCompleted, dbSizeBytes };
@@ -21989,10 +22220,10 @@ var init_central_core = __esm({
21989
22220
  */
21990
22221
  async generateProjectName(projectPath) {
21991
22222
  try {
21992
- const { execFile: execFile11 } = await import("node:child_process");
21993
- const { promisify: promisify20 } = await import("node:util");
21994
- const execFileAsync8 = promisify20(execFile11);
21995
- const { stdout } = await execFileAsync8(
22223
+ const { execFile: execFile12 } = await import("node:child_process");
22224
+ const { promisify: promisify21 } = await import("node:util");
22225
+ const execFileAsync9 = promisify21(execFile12);
22226
+ const { stdout } = await execFileAsync9(
21996
22227
  "git",
21997
22228
  ["remote", "get-url", "origin"],
21998
22229
  { cwd: projectPath, timeout: 5e3 }
@@ -22294,7 +22525,7 @@ var init_central_core = __esm({
22294
22525
  exportedAt: snapshot.exportedAt,
22295
22526
  version: 1
22296
22527
  };
22297
- const checksum = createHash3("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22528
+ const checksum = createHash4("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22298
22529
  return this.applyRemoteSettings({ ...payloadWithoutChecksum, checksum });
22299
22530
  }
22300
22531
  getAuthMaterialSnapshot(providerAuth) {
@@ -22337,7 +22568,7 @@ var init_central_core = __esm({
22337
22568
  exportedAt,
22338
22569
  version: 1
22339
22570
  };
22340
- const checksum = createHash3("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22571
+ const checksum = createHash4("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22341
22572
  return {
22342
22573
  ...payloadWithoutChecksum,
22343
22574
  checksum
@@ -22372,7 +22603,7 @@ var init_central_core = __esm({
22372
22603
  exportedAt: payload.exportedAt,
22373
22604
  version: payload.version
22374
22605
  };
22375
- const computedChecksum = createHash3("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22606
+ const computedChecksum = createHash4("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22376
22607
  if (computedChecksum !== payload.checksum) {
22377
22608
  return {
22378
22609
  success: false,
@@ -22494,13 +22725,13 @@ var init_central_core = __esm({
22494
22725
  });
22495
22726
 
22496
22727
  // ../core/src/sqlite-validation.ts
22497
- import { existsSync as existsSync8, statSync as statSync2 } from "node:fs";
22728
+ import { existsSync as existsSync8, statSync as statSync3 } from "node:fs";
22498
22729
  function isValidSqliteDatabaseFile(dbPath) {
22499
22730
  if (!existsSync8(dbPath)) {
22500
22731
  return false;
22501
22732
  }
22502
22733
  try {
22503
- if (!statSync2(dbPath).isFile()) {
22734
+ if (!statSync3(dbPath).isFile()) {
22504
22735
  return false;
22505
22736
  }
22506
22737
  } catch {
@@ -22693,10 +22924,10 @@ var init_migration = __esm({
22693
22924
  return basename3(projectPath);
22694
22925
  }
22695
22926
  try {
22696
- const { execFile: execFile11 } = await import("node:child_process");
22697
- const { promisify: promisify20 } = await import("node:util");
22698
- const execFileAsync8 = promisify20(execFile11);
22699
- const { stdout } = await execFileAsync8(
22927
+ const { execFile: execFile12 } = await import("node:child_process");
22928
+ const { promisify: promisify21 } = await import("node:util");
22929
+ const execFileAsync9 = promisify21(execFile12);
22930
+ const { stdout } = await execFileAsync9(
22700
22931
  "git",
22701
22932
  ["remote", "get-url", "origin"],
22702
22933
  { cwd: projectPath, timeout: 1e3 }
@@ -32969,7 +33200,7 @@ var init_memory_dreams = __esm({
32969
33200
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir6, access as access3, constants, readdir as readdir4, stat as stat2 } from "node:fs/promises";
32970
33201
  import { existsSync as existsSync11 } from "node:fs";
32971
33202
  import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute4, join as join15, normalize as normalize2, relative, resolve as resolve7, sep as sep3 } from "node:path";
32972
- import { createHash as createHash4 } from "node:crypto";
33203
+ import { createHash as createHash5 } from "node:crypto";
32973
33204
  function shouldSkipBackgroundQmdRefresh() {
32974
33205
  return (process.env.VITEST === "true" || process.env.NODE_ENV === "test") && process.env.FUSION_ENABLE_QMD_REFRESH_IN_TESTS !== "1";
32975
33206
  }
@@ -32985,7 +33216,7 @@ function memoryDreamsPath(rootDir) {
32985
33216
  function qmdMemoryCollectionName(rootDir) {
32986
33217
  const absoluteRoot = resolve7(rootDir);
32987
33218
  const slug = basename4(absoluteRoot).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
32988
- const hash = createHash4("sha1").update(absoluteRoot).digest("hex").slice(0, 12);
33219
+ const hash = createHash5("sha1").update(absoluteRoot).digest("hex").slice(0, 12);
32989
33220
  return `${QMD_COLLECTION_PREFIX}-${slug}-${hash}`;
32990
33221
  }
32991
33222
  function buildQmdSearchArgs(rootDir, options) {
@@ -33021,7 +33252,7 @@ function buildQmdRefreshCommands(rootDir) {
33021
33252
  function qmdAgentMemoryCollectionName(rootDir, agentId) {
33022
33253
  const absoluteRoot = resolve7(rootDir);
33023
33254
  const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
33024
- const hash = createHash4("sha1").update(`${absoluteRoot}:${agentId}`).digest("hex").slice(0, 12);
33255
+ const hash = createHash5("sha1").update(`${absoluteRoot}:${agentId}`).digest("hex").slice(0, 12);
33025
33256
  return `fusion-agent-memory-${safeAgentId.toLowerCase()}-${hash}`;
33026
33257
  }
33027
33258
  function dailyMemoryPath(rootDir, date = /* @__PURE__ */ new Date()) {
@@ -33425,13 +33656,13 @@ async function searchWithQmd(rootDir, options) {
33425
33656
  const command = "qmd";
33426
33657
  const limit = Math.max(1, Math.min(options.limit ?? 5, 20));
33427
33658
  try {
33428
- const { execFile: execFile11 } = await import("node:child_process");
33429
- const { promisify: promisify20 } = await import("node:util");
33430
- const execFileAsync8 = promisify20(execFile11);
33431
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync8);
33659
+ const { execFile: execFile12 } = await import("node:child_process");
33660
+ const { promisify: promisify21 } = await import("node:util");
33661
+ const execFileAsync9 = promisify21(execFile12);
33662
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync9);
33432
33663
  scheduleQmdProjectMemoryRefresh(rootDir);
33433
33664
  const args = buildQmdSearchArgs(rootDir, options);
33434
- const { stdout } = await execFileAsync8(command, args, {
33665
+ const { stdout } = await execFileAsync9(command, args, {
33435
33666
  cwd: rootDir,
33436
33667
  timeout: 4e3,
33437
33668
  maxBuffer: 1024 * 1024
@@ -33456,12 +33687,12 @@ async function searchWithQmd(rootDir, options) {
33456
33687
  return [];
33457
33688
  }
33458
33689
  }
33459
- async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync8) {
33690
+ async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync9) {
33460
33691
  const collectionName = qmdMemoryCollectionName(rootDir);
33461
33692
  const memoryDir = memoryWorkspacePath(rootDir);
33462
33693
  await mkdir6(memoryDir, { recursive: true });
33463
33694
  try {
33464
- await execFileAsync8("qmd", buildQmdCollectionAddArgs(rootDir), {
33695
+ await execFileAsync9("qmd", buildQmdCollectionAddArgs(rootDir), {
33465
33696
  cwd: rootDir,
33466
33697
  timeout: 4e3,
33467
33698
  maxBuffer: 512 * 1024
@@ -33477,9 +33708,9 @@ ${stderr}`)) {
33477
33708
  return collectionName;
33478
33709
  }
33479
33710
  async function getDefaultExecFileAsync() {
33480
- const { execFile: execFile11 } = await import("node:child_process");
33481
- const { promisify: promisify20 } = await import("node:util");
33482
- return promisify20(execFile11);
33711
+ const { execFile: execFile12 } = await import("node:child_process");
33712
+ const { promisify: promisify21 } = await import("node:util");
33713
+ return promisify21(execFile12);
33483
33714
  }
33484
33715
  async function refreshQmdProjectMemoryIndex(rootDir, options) {
33485
33716
  const key = resolve7(rootDir);
@@ -33494,14 +33725,14 @@ async function refreshQmdProjectMemoryIndex(rootDir, options) {
33494
33725
  }
33495
33726
  }
33496
33727
  const promise = (async () => {
33497
- const execFileAsync8 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33498
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync8);
33499
- await execFileAsync8("qmd", ["update"], {
33728
+ const execFileAsync9 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33729
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync9);
33730
+ await execFileAsync9("qmd", ["update"], {
33500
33731
  cwd: rootDir,
33501
33732
  timeout: 3e4,
33502
33733
  maxBuffer: 1024 * 1024
33503
33734
  });
33504
- await execFileAsync8("qmd", ["embed"], {
33735
+ await execFileAsync9("qmd", ["embed"], {
33505
33736
  cwd: rootDir,
33506
33737
  timeout: 12e4,
33507
33738
  maxBuffer: 1024 * 1024
@@ -33537,12 +33768,12 @@ async function refreshQmdAgentMemoryIndex(rootDir, agentId, options) {
33537
33768
  }
33538
33769
  }
33539
33770
  const promise = (async () => {
33540
- const execFileAsync8 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33771
+ const execFileAsync9 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33541
33772
  const { agentMemoryWorkspacePath: agentMemoryWorkspacePath2 } = await Promise.resolve().then(() => (init_memory_dreams(), memory_dreams_exports));
33542
33773
  const workspacePath = agentMemoryWorkspacePath2(rootDir, agentId);
33543
33774
  await mkdir6(workspacePath, { recursive: true });
33544
33775
  try {
33545
- await execFileAsync8("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
33776
+ await execFileAsync9("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
33546
33777
  cwd: rootDir,
33547
33778
  timeout: 4e3,
33548
33779
  maxBuffer: 512 * 1024
@@ -33555,8 +33786,8 @@ ${stderr}`)) {
33555
33786
  throw err;
33556
33787
  }
33557
33788
  }
33558
- await execFileAsync8("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
33559
- await execFileAsync8("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
33789
+ await execFileAsync9("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
33790
+ await execFileAsync9("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
33560
33791
  })();
33561
33792
  qmdAgentRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
33562
33793
  try {
@@ -33577,8 +33808,8 @@ function scheduleQmdAgentMemoryRefresh(rootDir, agentId) {
33577
33808
  }
33578
33809
  async function isQmdAvailable() {
33579
33810
  try {
33580
- const execFileAsync8 = await getDefaultExecFileAsync();
33581
- await execFileAsync8("qmd", ["--help"], {
33811
+ const execFileAsync9 = await getDefaultExecFileAsync();
33812
+ await execFileAsync9("qmd", ["--help"], {
33582
33813
  timeout: 3e3,
33583
33814
  maxBuffer: 128 * 1024
33584
33815
  });
@@ -33588,12 +33819,12 @@ async function isQmdAvailable() {
33588
33819
  }
33589
33820
  }
33590
33821
  async function installQmd(options) {
33591
- const execFileAsync8 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33822
+ const execFileAsync9 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33592
33823
  const [command, ...args] = QMD_INSTALL_COMMAND.split(" ");
33593
33824
  if (!command || args.length === 0) {
33594
33825
  throw new MemoryBackendError("BACKEND_UNAVAILABLE", "qmd install command is not configured", "qmd");
33595
33826
  }
33596
- await execFileAsync8(command, args, {
33827
+ await execFileAsync9(command, args, {
33597
33828
  timeout: 12e4,
33598
33829
  maxBuffer: 1024 * 1024
33599
33830
  });
@@ -34894,6 +35125,135 @@ function resolveLocalNodeId(nodes, fallback2 = "local") {
34894
35125
  const localNode = nodes?.find((node) => node.type === "local");
34895
35126
  return localNode?.id ?? fallback2;
34896
35127
  }
35128
+ function parseTaskId(taskId) {
35129
+ const match = taskId.trim().toUpperCase().match(TASK_ID_PATTERN);
35130
+ if (!match) {
35131
+ return null;
35132
+ }
35133
+ const sequence = Number.parseInt(match[2], 10);
35134
+ if (!Number.isFinite(sequence)) {
35135
+ return null;
35136
+ }
35137
+ return { prefix: match[1], sequence };
35138
+ }
35139
+ function getConfiguredPrefixAndLegacyNextId(db) {
35140
+ try {
35141
+ const row = db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get();
35142
+ if (!row) {
35143
+ return { prefix: "KB", nextId: null };
35144
+ }
35145
+ const settings = row.settings ? JSON.parse(row.settings) : null;
35146
+ return {
35147
+ prefix: (settings?.taskPrefix ?? "KB").trim().toUpperCase(),
35148
+ nextId: typeof row.nextId === "number" ? row.nextId : null
35149
+ };
35150
+ } catch {
35151
+ return { prefix: "KB", nextId: null };
35152
+ }
35153
+ }
35154
+ function getKnownPrefixes(db) {
35155
+ const prefixes = /* @__PURE__ */ new Set();
35156
+ const configured = getConfiguredPrefixAndLegacyNextId(db).prefix;
35157
+ if (configured) {
35158
+ prefixes.add(configured);
35159
+ }
35160
+ const addFromQuery = (sql, mapper) => {
35161
+ try {
35162
+ const rows = db.prepare(sql).all();
35163
+ for (const row of rows) {
35164
+ const prefix = mapper(row)?.trim().toUpperCase();
35165
+ if (prefix) {
35166
+ prefixes.add(prefix);
35167
+ }
35168
+ }
35169
+ } catch {
35170
+ }
35171
+ };
35172
+ addFromQuery("SELECT prefix FROM distributed_task_id_state", (row) => row.prefix);
35173
+ addFromQuery("SELECT prefix FROM distributed_task_id_reservations", (row) => row.prefix);
35174
+ addFromQuery("SELECT id FROM tasks", (row) => parseTaskId(String(row.id ?? ""))?.prefix);
35175
+ addFromQuery("SELECT id FROM archivedTasks", (row) => parseTaskId(String(row.id ?? ""))?.prefix);
35176
+ return prefixes;
35177
+ }
35178
+ function getMaxTaskSequenceFromTable(db, table, prefix) {
35179
+ try {
35180
+ const rows = db.prepare(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${prefix}-%`);
35181
+ let maxSequence = 0;
35182
+ for (const row of rows) {
35183
+ const parsed = parseTaskId(row.id);
35184
+ if (parsed?.prefix === prefix && parsed.sequence > maxSequence) {
35185
+ maxSequence = parsed.sequence;
35186
+ }
35187
+ }
35188
+ return maxSequence;
35189
+ } catch {
35190
+ return 0;
35191
+ }
35192
+ }
35193
+ function getMaxReservationSequence(db, prefix) {
35194
+ try {
35195
+ const row = db.prepare("SELECT MAX(sequence) AS maxSeq FROM distributed_task_id_reservations WHERE prefix = ?").get(prefix);
35196
+ return typeof row?.maxSeq === "number" ? row.maxSeq : 0;
35197
+ } catch {
35198
+ return 0;
35199
+ }
35200
+ }
35201
+ function getNextSequenceFloor(db, prefix) {
35202
+ const configured = getConfiguredPrefixAndLegacyNextId(db);
35203
+ let nextSequence = 1;
35204
+ if (configured.prefix === prefix && configured.nextId && configured.nextId > nextSequence) {
35205
+ nextSequence = configured.nextId;
35206
+ }
35207
+ const taskHighWaterMark = getMaxTaskSequenceFromTable(db, "tasks", prefix) + 1;
35208
+ const archivedHighWaterMark = getMaxTaskSequenceFromTable(db, "archivedTasks", prefix) + 1;
35209
+ const reservationHighWaterMark = getMaxReservationSequence(db, prefix) + 1;
35210
+ nextSequence = Math.max(nextSequence, taskHighWaterMark, archivedHighWaterMark, reservationHighWaterMark);
35211
+ return nextSequence;
35212
+ }
35213
+ function ensureStateRow(db, prefix) {
35214
+ const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
35215
+ const nextSequence = getNextSequenceFloor(db, prefix);
35216
+ db.prepare(
35217
+ `INSERT OR IGNORE INTO distributed_task_id_state (
35218
+ prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
35219
+ ) VALUES (?, ?, 0, NULL, ?)`
35220
+ ).run(prefix, nextSequence, nowIso3);
35221
+ db.prepare(
35222
+ `UPDATE distributed_task_id_state
35223
+ SET nextSequence = MAX(nextSequence, ?),
35224
+ updatedAt = ?
35225
+ WHERE prefix = ?`
35226
+ ).run(nextSequence, nowIso3, prefix);
35227
+ }
35228
+ function reconcileTaskIdState(db) {
35229
+ const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
35230
+ return db.transaction(() => {
35231
+ const reconciled = [];
35232
+ for (const prefix of getKnownPrefixes(db)) {
35233
+ const nextSequence = getNextSequenceFloor(db, prefix);
35234
+ db.prepare(
35235
+ `INSERT OR IGNORE INTO distributed_task_id_state (
35236
+ prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
35237
+ ) VALUES (?, ?, 0, NULL, ?)`
35238
+ ).run(prefix, nextSequence, nowIso3);
35239
+ const before = db.prepare("SELECT nextSequence FROM distributed_task_id_state WHERE prefix = ?").get(prefix);
35240
+ db.prepare(
35241
+ `UPDATE distributed_task_id_state
35242
+ SET nextSequence = MAX(nextSequence, ?),
35243
+ updatedAt = ?
35244
+ WHERE prefix = ?`
35245
+ ).run(nextSequence, nowIso3, prefix);
35246
+ const after = db.prepare("SELECT nextSequence FROM distributed_task_id_state WHERE prefix = ?").get(prefix);
35247
+ if (!before || !after || after.nextSequence !== before.nextSequence) {
35248
+ reconciled.push(prefix);
35249
+ }
35250
+ }
35251
+ if (reconciled.length > 0) {
35252
+ db.bumpLastModified();
35253
+ }
35254
+ return reconciled;
35255
+ });
35256
+ }
34897
35257
  function formatDistributedTaskId(prefix, sequence) {
34898
35258
  const normalizedPrefix = prefix.trim().toUpperCase();
34899
35259
  if (!normalizedPrefix) {
@@ -34936,48 +35296,6 @@ function createDistributedTaskIdAllocator(db) {
34936
35296
  };
34937
35297
  return existsInTable("tasks") || existsInTable("archivedTasks");
34938
35298
  };
34939
- const ensureStateRow = (prefix) => {
34940
- let seedSequence = 1;
34941
- try {
34942
- const configRow = db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get();
34943
- if (configRow) {
34944
- const settings = configRow.settings ? JSON.parse(configRow.settings) : null;
34945
- const configuredPrefix = (settings?.taskPrefix ?? "KB").trim().toUpperCase();
34946
- if (configuredPrefix === prefix && typeof configRow.nextId === "number" && configRow.nextId > seedSequence) {
34947
- seedSequence = configRow.nextId;
34948
- }
34949
- }
34950
- } catch {
34951
- }
34952
- const idPattern = `${prefix}-%`;
34953
- const probeTable = (table) => {
34954
- try {
34955
- const row = db.prepare(
34956
- `SELECT MAX(CAST(substr(id, ${prefix.length + 2}) AS INTEGER)) AS maxSeq
34957
- FROM ${table}
34958
- WHERE id LIKE ? AND substr(id, ${prefix.length + 2}) GLOB '[0-9]*'`
34959
- ).get(idPattern);
34960
- if (row && typeof row.maxSeq === "number" && row.maxSeq + 1 > seedSequence) {
34961
- seedSequence = row.maxSeq + 1;
34962
- }
34963
- } catch {
34964
- }
34965
- };
34966
- probeTable("tasks");
34967
- probeTable("archivedTasks");
34968
- const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
34969
- db.prepare(
34970
- `INSERT OR IGNORE INTO distributed_task_id_state (
34971
- prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
34972
- ) VALUES (?, ?, 0, NULL, ?)`
34973
- ).run(prefix, seedSequence, nowIso3);
34974
- db.prepare(
34975
- `UPDATE distributed_task_id_state
34976
- SET nextSequence = MAX(nextSequence, ?),
34977
- updatedAt = ?
34978
- WHERE prefix = ?`
34979
- ).run(seedSequence, nowIso3, prefix);
34980
- };
34981
35299
  return {
34982
35300
  formatDistributedTaskId,
34983
35301
  reserveDistributedTaskId: async (input) => withLock(async () => {
@@ -34991,7 +35309,7 @@ function createDistributedTaskIdAllocator(db) {
34991
35309
  if (!prefix) {
34992
35310
  throw new DistributedTaskIdError("prefix is required", "invalid_prefix");
34993
35311
  }
34994
- ensureStateRow(prefix);
35312
+ ensureStateRow(db, prefix);
34995
35313
  const state = db.prepare(
34996
35314
  "SELECT nextSequence, committedClusterTaskCount FROM distributed_task_id_state WHERE prefix = ?"
34997
35315
  ).get(prefix);
@@ -35045,7 +35363,7 @@ function createDistributedTaskIdAllocator(db) {
35045
35363
  SET status = 'committed', committedAt = ?, updatedAt = ?
35046
35364
  WHERE reservationId = ?`
35047
35365
  ).run(nowIso3, nowIso3, row.reservationId);
35048
- ensureStateRow(row.prefix);
35366
+ ensureStateRow(db, row.prefix);
35049
35367
  db.prepare(
35050
35368
  `UPDATE distributed_task_id_state
35051
35369
  SET committedClusterTaskCount = committedClusterTaskCount + 1,
@@ -35091,7 +35409,7 @@ function createDistributedTaskIdAllocator(db) {
35091
35409
  WHERE reservationId = ?`
35092
35410
  ).run(input.reason, nowIso3, nowIso3, row.reservationId);
35093
35411
  }
35094
- ensureStateRow(row.prefix);
35412
+ ensureStateRow(db, row.prefix);
35095
35413
  const state = db.prepare(
35096
35414
  "SELECT committedClusterTaskCount FROM distributed_task_id_state WHERE prefix = ?"
35097
35415
  ).get(row.prefix);
@@ -35113,7 +35431,7 @@ function createDistributedTaskIdAllocator(db) {
35113
35431
  if (!prefix) {
35114
35432
  throw new DistributedTaskIdError("prefix is required", "invalid_prefix");
35115
35433
  }
35116
- ensureStateRow(prefix);
35434
+ ensureStateRow(db, prefix);
35117
35435
  const row = db.prepare(
35118
35436
  `SELECT nextSequence, committedClusterTaskCount, lastCommittedTaskId
35119
35437
  FROM distributed_task_id_state
@@ -35138,11 +35456,12 @@ function createDistributedTaskIdAllocator(db) {
35138
35456
  })
35139
35457
  };
35140
35458
  }
35141
- var DEFAULT_RESERVATION_TTL_MS, DistributedTaskIdError;
35459
+ var DEFAULT_RESERVATION_TTL_MS, TASK_ID_PATTERN, DistributedTaskIdError;
35142
35460
  var init_distributed_task_id = __esm({
35143
35461
  "../core/src/distributed-task-id.ts"() {
35144
35462
  "use strict";
35145
35463
  DEFAULT_RESERVATION_TTL_MS = 15 * 60 * 1e3;
35464
+ TASK_ID_PATTERN = /^([A-Z][A-Z0-9]*)-(\d+)$/;
35146
35465
  DistributedTaskIdError = class extends Error {
35147
35466
  constructor(message, code) {
35148
35467
  super(message);
@@ -35437,6 +35756,8 @@ var init_store = __esm({
35437
35756
  worktreeAllocationLock = Promise.resolve();
35438
35757
  /** Promise chain for serializing config.json read-modify-write cycles */
35439
35758
  configLock = Promise.resolve();
35759
+ /** Startup/open guard for distributed_task_id_state reconciliation. */
35760
+ taskIdStateReconciled = false;
35440
35761
  /** Cached workflow steps — invalidated on create/update/delete */
35441
35762
  workflowStepsCache = null;
35442
35763
  /** Plugin-contributed workflow step templates injected by engine runtime. */
@@ -35503,6 +35824,7 @@ var init_store = __esm({
35503
35824
  throw error;
35504
35825
  }
35505
35826
  this._db = db;
35827
+ this.reconcileDistributedTaskIdStateOnOpen();
35506
35828
  if (detectLegacyData(this.fusionDir)) {
35507
35829
  }
35508
35830
  }
@@ -35522,6 +35844,13 @@ var init_store = __esm({
35522
35844
  }
35523
35845
  return this._archiveDb;
35524
35846
  }
35847
+ reconcileDistributedTaskIdStateOnOpen() {
35848
+ if (this.taskIdStateReconciled) {
35849
+ return;
35850
+ }
35851
+ reconcileTaskIdState(this.db);
35852
+ this.taskIdStateReconciled = true;
35853
+ }
35525
35854
  async init() {
35526
35855
  await mkdir7(this.tasksDir, { recursive: true });
35527
35856
  if (!this._db) {
@@ -35534,15 +35863,18 @@ var init_store = __esm({
35534
35863
  }
35535
35864
  this._db = db;
35536
35865
  }
35866
+ this.reconcileDistributedTaskIdStateOnOpen();
35537
35867
  if (detectLegacyData(this.fusionDir)) {
35538
35868
  await migrateFromLegacy(this.fusionDir, this._db);
35539
35869
  }
35540
35870
  await this.migrateActiveArchivedTasksToArchiveDb();
35541
35871
  await this.importLegacyAgentLogsOnce();
35872
+ this.taskIdStateReconciled = false;
35873
+ this.reconcileDistributedTaskIdStateOnOpen();
35542
35874
  if (!existsSync13(this.configPath)) {
35543
35875
  const config = await this.readConfig();
35544
35876
  try {
35545
- await writeFile6(this.configPath, JSON.stringify(config, null, 2));
35877
+ await writeFile6(this.configPath, this.serializeConfigForDisk(config));
35546
35878
  } catch (err) {
35547
35879
  storeLog.warn("Backward-compat config.json sync failed during init", {
35548
35880
  phase: "init:config-sync",
@@ -36131,117 +36463,8 @@ ${outcome}`;
36131
36463
  `;
36132
36464
  return [...columns, limitedLog].join(", ");
36133
36465
  }
36134
- /**
36135
- * Upsert a task to the database. Used by create and update operations.
36136
- */
36137
- upsertTask(task) {
36138
- this.db.prepare(`
36139
- INSERT INTO tasks (
36140
- id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
36141
- worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
36142
- modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
36143
- workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
36144
- summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
36145
- tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
36146
- executionStartedAt, executionCompletedAt,
36147
- dependencies, steps, log, attachments, steeringComments,
36148
- comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
36149
- sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
36150
- mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
36151
- ) VALUES (
36152
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
36153
- )
36154
- ON CONFLICT(id) DO UPDATE SET
36155
- lineageId = excluded.lineageId,
36156
- title = excluded.title,
36157
- description = excluded.description,
36158
- priority = excluded.priority,
36159
- "column" = excluded."column",
36160
- status = excluded.status,
36161
- size = excluded.size,
36162
- reviewLevel = excluded.reviewLevel,
36163
- currentStep = excluded.currentStep,
36164
- worktree = excluded.worktree,
36165
- blockedBy = excluded.blockedBy,
36166
- paused = excluded.paused,
36167
- baseBranch = excluded.baseBranch,
36168
- branch = excluded.branch,
36169
- executionStartBranch = excluded.executionStartBranch,
36170
- baseCommitSha = excluded.baseCommitSha,
36171
- modelPresetId = excluded.modelPresetId,
36172
- modelProvider = excluded.modelProvider,
36173
- modelId = excluded.modelId,
36174
- validatorModelProvider = excluded.validatorModelProvider,
36175
- validatorModelId = excluded.validatorModelId,
36176
- planningModelProvider = excluded.planningModelProvider,
36177
- planningModelId = excluded.planningModelId,
36178
- mergeRetries = excluded.mergeRetries,
36179
- workflowStepRetries = excluded.workflowStepRetries,
36180
- stuckKillCount = excluded.stuckKillCount,
36181
- postReviewFixCount = excluded.postReviewFixCount,
36182
- recoveryRetryCount = excluded.recoveryRetryCount,
36183
- taskDoneRetryCount = excluded.taskDoneRetryCount,
36184
- verificationFailureCount = excluded.verificationFailureCount,
36185
- mergeConflictBounceCount = excluded.mergeConflictBounceCount,
36186
- nextRecoveryAt = excluded.nextRecoveryAt,
36187
- error = excluded.error,
36188
- summary = excluded.summary,
36189
- thinkingLevel = excluded.thinkingLevel,
36190
- executionMode = excluded.executionMode,
36191
- tokenUsageInputTokens = excluded.tokenUsageInputTokens,
36192
- tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
36193
- tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
36194
- tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
36195
- tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
36196
- tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
36197
- createdAt = excluded.createdAt,
36198
- updatedAt = excluded.updatedAt,
36199
- columnMovedAt = excluded.columnMovedAt,
36200
- executionStartedAt = excluded.executionStartedAt,
36201
- executionCompletedAt = excluded.executionCompletedAt,
36202
- dependencies = excluded.dependencies,
36203
- steps = excluded.steps,
36204
- log = excluded.log,
36205
- attachments = excluded.attachments,
36206
- steeringComments = excluded.steeringComments,
36207
- comments = excluded.comments,
36208
- review = excluded.review,
36209
- reviewState = excluded.reviewState,
36210
- workflowStepResults = excluded.workflowStepResults,
36211
- prInfo = excluded.prInfo,
36212
- issueInfo = excluded.issueInfo,
36213
- githubTracking = excluded.githubTracking,
36214
- sourceIssueProvider = excluded.sourceIssueProvider,
36215
- sourceIssueRepository = excluded.sourceIssueRepository,
36216
- sourceIssueExternalIssueId = excluded.sourceIssueExternalIssueId,
36217
- sourceIssueNumber = excluded.sourceIssueNumber,
36218
- sourceIssueUrl = excluded.sourceIssueUrl,
36219
- mergeDetails = excluded.mergeDetails,
36220
- breakIntoSubtasks = excluded.breakIntoSubtasks,
36221
- enabledWorkflowSteps = excluded.enabledWorkflowSteps,
36222
- modifiedFiles = excluded.modifiedFiles,
36223
- missionId = excluded.missionId,
36224
- sliceId = excluded.sliceId,
36225
- assignedAgentId = excluded.assignedAgentId,
36226
- pausedByAgentId = excluded.pausedByAgentId,
36227
- assigneeUserId = excluded.assigneeUserId,
36228
- nodeId = excluded.nodeId,
36229
- effectiveNodeId = excluded.effectiveNodeId,
36230
- effectiveNodeSource = excluded.effectiveNodeSource,
36231
- sourceType = excluded.sourceType,
36232
- sourceAgentId = excluded.sourceAgentId,
36233
- sourceRunId = excluded.sourceRunId,
36234
- sourceSessionId = excluded.sourceSessionId,
36235
- sourceMessageId = excluded.sourceMessageId,
36236
- sourceParentTaskId = excluded.sourceParentTaskId,
36237
- sourceMetadata = excluded.sourceMetadata,
36238
- checkedOutBy = excluded.checkedOutBy,
36239
- checkedOutAt = excluded.checkedOutAt,
36240
- checkoutNodeId = excluded.checkoutNodeId,
36241
- checkoutRunId = excluded.checkoutRunId,
36242
- checkoutLeaseRenewedAt = excluded.checkoutLeaseRenewedAt,
36243
- checkoutLeaseEpoch = excluded.checkoutLeaseEpoch
36244
- `).run(
36466
+ getTaskPersistValues(task) {
36467
+ return [
36245
36468
  task.id,
36246
36469
  task.lineageId ?? generateTaskLineageId(),
36247
36470
  task.title ?? null,
@@ -36332,9 +36555,193 @@ ${outcome}`;
36332
36555
  task.checkoutRunId ?? null,
36333
36556
  task.checkoutLeaseRenewedAt ?? null,
36334
36557
  task.checkoutLeaseEpoch ?? 0
36335
- );
36558
+ ];
36559
+ }
36560
+ /**
36561
+ * Insert a brand-new task row. Create paths must use this so SQLite raises on
36562
+ * duplicate IDs instead of silently rewriting the existing row.
36563
+ */
36564
+ insertTask(task) {
36565
+ this.db.prepare(`
36566
+ INSERT INTO tasks (
36567
+ id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
36568
+ worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
36569
+ modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
36570
+ workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
36571
+ summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
36572
+ tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
36573
+ executionStartedAt, executionCompletedAt,
36574
+ dependencies, steps, log, attachments, steeringComments,
36575
+ comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
36576
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
36577
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
36578
+ ) VALUES (
36579
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
36580
+ )
36581
+ `).run(...this.getTaskPersistValues(task));
36582
+ this.db.bumpLastModified();
36583
+ }
36584
+ /**
36585
+ * Upsert a task to the database. Update paths intentionally retain ON CONFLICT
36586
+ * semantics; create paths must use insertTask() instead.
36587
+ */
36588
+ upsertTask(task) {
36589
+ this.db.prepare(`
36590
+ INSERT INTO tasks (
36591
+ id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
36592
+ worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
36593
+ modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
36594
+ workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
36595
+ summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
36596
+ tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
36597
+ executionStartedAt, executionCompletedAt,
36598
+ dependencies, steps, log, attachments, steeringComments,
36599
+ comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
36600
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
36601
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
36602
+ ) VALUES (
36603
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
36604
+ )
36605
+ ON CONFLICT(id) DO UPDATE SET
36606
+ lineageId = excluded.lineageId,
36607
+ title = excluded.title,
36608
+ description = excluded.description,
36609
+ priority = excluded.priority,
36610
+ "column" = excluded."column",
36611
+ status = excluded.status,
36612
+ size = excluded.size,
36613
+ reviewLevel = excluded.reviewLevel,
36614
+ currentStep = excluded.currentStep,
36615
+ worktree = excluded.worktree,
36616
+ blockedBy = excluded.blockedBy,
36617
+ paused = excluded.paused,
36618
+ baseBranch = excluded.baseBranch,
36619
+ branch = excluded.branch,
36620
+ executionStartBranch = excluded.executionStartBranch,
36621
+ baseCommitSha = excluded.baseCommitSha,
36622
+ modelPresetId = excluded.modelPresetId,
36623
+ modelProvider = excluded.modelProvider,
36624
+ modelId = excluded.modelId,
36625
+ validatorModelProvider = excluded.validatorModelProvider,
36626
+ validatorModelId = excluded.validatorModelId,
36627
+ planningModelProvider = excluded.planningModelProvider,
36628
+ planningModelId = excluded.planningModelId,
36629
+ mergeRetries = excluded.mergeRetries,
36630
+ workflowStepRetries = excluded.workflowStepRetries,
36631
+ stuckKillCount = excluded.stuckKillCount,
36632
+ postReviewFixCount = excluded.postReviewFixCount,
36633
+ recoveryRetryCount = excluded.recoveryRetryCount,
36634
+ taskDoneRetryCount = excluded.taskDoneRetryCount,
36635
+ verificationFailureCount = excluded.verificationFailureCount,
36636
+ mergeConflictBounceCount = excluded.mergeConflictBounceCount,
36637
+ nextRecoveryAt = excluded.nextRecoveryAt,
36638
+ error = excluded.error,
36639
+ summary = excluded.summary,
36640
+ thinkingLevel = excluded.thinkingLevel,
36641
+ executionMode = excluded.executionMode,
36642
+ tokenUsageInputTokens = excluded.tokenUsageInputTokens,
36643
+ tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
36644
+ tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
36645
+ tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
36646
+ tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
36647
+ tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
36648
+ createdAt = excluded.createdAt,
36649
+ updatedAt = excluded.updatedAt,
36650
+ columnMovedAt = excluded.columnMovedAt,
36651
+ executionStartedAt = excluded.executionStartedAt,
36652
+ executionCompletedAt = excluded.executionCompletedAt,
36653
+ dependencies = excluded.dependencies,
36654
+ steps = excluded.steps,
36655
+ log = excluded.log,
36656
+ attachments = excluded.attachments,
36657
+ steeringComments = excluded.steeringComments,
36658
+ comments = excluded.comments,
36659
+ review = excluded.review,
36660
+ reviewState = excluded.reviewState,
36661
+ workflowStepResults = excluded.workflowStepResults,
36662
+ prInfo = excluded.prInfo,
36663
+ issueInfo = excluded.issueInfo,
36664
+ githubTracking = excluded.githubTracking,
36665
+ sourceIssueProvider = excluded.sourceIssueProvider,
36666
+ sourceIssueRepository = excluded.sourceIssueRepository,
36667
+ sourceIssueExternalIssueId = excluded.sourceIssueExternalIssueId,
36668
+ sourceIssueNumber = excluded.sourceIssueNumber,
36669
+ sourceIssueUrl = excluded.sourceIssueUrl,
36670
+ mergeDetails = excluded.mergeDetails,
36671
+ breakIntoSubtasks = excluded.breakIntoSubtasks,
36672
+ enabledWorkflowSteps = excluded.enabledWorkflowSteps,
36673
+ modifiedFiles = excluded.modifiedFiles,
36674
+ missionId = excluded.missionId,
36675
+ sliceId = excluded.sliceId,
36676
+ assignedAgentId = excluded.assignedAgentId,
36677
+ pausedByAgentId = excluded.pausedByAgentId,
36678
+ assigneeUserId = excluded.assigneeUserId,
36679
+ nodeId = excluded.nodeId,
36680
+ effectiveNodeId = excluded.effectiveNodeId,
36681
+ effectiveNodeSource = excluded.effectiveNodeSource,
36682
+ sourceType = excluded.sourceType,
36683
+ sourceAgentId = excluded.sourceAgentId,
36684
+ sourceRunId = excluded.sourceRunId,
36685
+ sourceSessionId = excluded.sourceSessionId,
36686
+ sourceMessageId = excluded.sourceMessageId,
36687
+ sourceParentTaskId = excluded.sourceParentTaskId,
36688
+ sourceMetadata = excluded.sourceMetadata,
36689
+ checkedOutBy = excluded.checkedOutBy,
36690
+ checkedOutAt = excluded.checkedOutAt,
36691
+ checkoutNodeId = excluded.checkoutNodeId,
36692
+ checkoutRunId = excluded.checkoutRunId,
36693
+ checkoutLeaseRenewedAt = excluded.checkoutLeaseRenewedAt,
36694
+ checkoutLeaseEpoch = excluded.checkoutLeaseEpoch
36695
+ `).run(...this.getTaskPersistValues(task));
36336
36696
  this.db.bumpLastModified();
36337
36697
  }
36698
+ isTaskIdConflictError(error) {
36699
+ const message = error instanceof Error ? error.message : String(error);
36700
+ return /SQLITE_CONSTRAINT|UNIQUE constraint failed: tasks\.id|PRIMARY KEY constraint failed: tasks\.id/i.test(message);
36701
+ }
36702
+ logTaskCreateConflict(task, operation, error) {
36703
+ storeLog.error("Refused colliding task create", {
36704
+ phase: "task-create:id-conflict",
36705
+ operation,
36706
+ taskId: task.id,
36707
+ column: task.column,
36708
+ sourceType: task.sourceType,
36709
+ error: error instanceof Error ? error.message : String(error)
36710
+ });
36711
+ }
36712
+ insertTaskWithFtsRecovery(task, operation) {
36713
+ const normalizeConflict = (error) => {
36714
+ this.logTaskCreateConflict(task, operation, error);
36715
+ throw new Error(`Task ID already exists: ${task.id}`);
36716
+ };
36717
+ try {
36718
+ this.insertTask(task);
36719
+ return;
36720
+ } catch (error) {
36721
+ if (this.isTaskIdConflictError(error)) {
36722
+ normalizeConflict(error);
36723
+ }
36724
+ if (!this.db.isFts5CorruptionError(error)) {
36725
+ throw error;
36726
+ }
36727
+ console.warn(`[fusion:store] FTS5 corruption detected during insert for task ${task.id}; rebuilding index and retrying once`);
36728
+ try {
36729
+ this.db.rebuildFts5Index();
36730
+ } catch (rebuildError) {
36731
+ console.warn("[fusion:store] FTS5 rebuild failed; propagating original insert error", rebuildError);
36732
+ throw error;
36733
+ }
36734
+ try {
36735
+ this.insertTask(task);
36736
+ } catch (retryError) {
36737
+ if (this.isTaskIdConflictError(retryError)) {
36738
+ normalizeConflict(retryError);
36739
+ }
36740
+ console.warn("[fusion:store] Insert retry after FTS5 rebuild failed; propagating original insert error", retryError);
36741
+ throw error;
36742
+ }
36743
+ }
36744
+ }
36338
36745
  upsertTaskWithFtsRecovery(task) {
36339
36746
  try {
36340
36747
  this.upsertTask(task);
@@ -36367,6 +36774,28 @@ ${outcome}`;
36367
36774
  if (!row) return void 0;
36368
36775
  return this.rowToTask(row);
36369
36776
  }
36777
+ isTaskIdPresentInArchivedTasksTable(id) {
36778
+ try {
36779
+ const row = this.db.prepare("SELECT 1 as found FROM archivedTasks WHERE id = ? LIMIT 1").get(id);
36780
+ return row?.found === 1;
36781
+ } catch {
36782
+ return false;
36783
+ }
36784
+ }
36785
+ taskIdExistsAnywhere(id) {
36786
+ if (this.readTaskFromDb(id)) {
36787
+ return true;
36788
+ }
36789
+ if (this.isTaskIdPresentInArchivedTasksTable(id)) {
36790
+ return true;
36791
+ }
36792
+ return this.archiveDb.get(id) !== void 0;
36793
+ }
36794
+ assertTaskIdAvailable(id) {
36795
+ if (this.taskIdExistsAnywhere(id)) {
36796
+ throw new Error(`Task ID already exists: ${id}`);
36797
+ }
36798
+ }
36370
36799
  isTaskArchived(id) {
36371
36800
  const row = this.db.prepare('SELECT "column" FROM tasks WHERE id = ?').get(id);
36372
36801
  if (row) {
@@ -36594,7 +37023,16 @@ ${outcome}`;
36594
37023
  await rename4(tmpPath, taskJsonPath);
36595
37024
  }
36596
37025
  /**
36597
- * Write a task to SQLite (primary store) and also write task.json to disk
37026
+ * Write a brand-new task to SQLite (primary store) and also write task.json to disk
37027
+ * for backward compatibility and debugging. Create paths must call this variant
37028
+ * so duplicate IDs fail safely instead of overwriting existing rows.
37029
+ */
37030
+ async atomicCreateTaskJson(dir2, task, operation) {
37031
+ this.insertTaskWithFtsRecovery(task, operation);
37032
+ await this.writeTaskJsonFile(dir2, task);
37033
+ }
37034
+ /**
37035
+ * Write an existing task to SQLite (primary store) and also write task.json to disk
36598
37036
  * for backward compatibility and debugging.
36599
37037
  */
36600
37038
  async atomicWriteTaskJson(dir2, task) {
@@ -36610,7 +37048,7 @@ ${outcome}`;
36610
37048
  * @param auditInput - Optional audit event input to record atomically with the task write
36611
37049
  */
36612
37050
  async atomicWriteTaskJsonWithAudit(dir2, task, auditInput) {
36613
- this.db.transaction(() => {
37051
+ this.db.transactionImmediate(() => {
36614
37052
  this.upsertTaskWithFtsRecovery(task);
36615
37053
  if (auditInput) {
36616
37054
  const eventId = randomUUID13();
@@ -36888,6 +37326,10 @@ ${outcome}`;
36888
37326
  settings: fromJson(row.settings)
36889
37327
  };
36890
37328
  }
37329
+ serializeConfigForDisk(config) {
37330
+ const { nextId: _deprecatedNextId, ...configForDisk } = config;
37331
+ return JSON.stringify(configForDisk, null, 2);
37332
+ }
36891
37333
  async writeConfig(config, options) {
36892
37334
  const now = (/* @__PURE__ */ new Date()).toISOString();
36893
37335
  const row = this.db.prepare("SELECT nextWorkflowStepId FROM config WHERE id = 1").get();
@@ -36895,10 +37337,14 @@ ${outcome}`;
36895
37337
  const legacyWorkflowSteps = config.workflowSteps;
36896
37338
  const workflowStepsJson = Array.isArray(legacyWorkflowSteps) ? JSON.stringify(legacyWorkflowSteps) : "[]";
36897
37339
  this.db.prepare(
36898
- `INSERT OR REPLACE INTO config (id, nextId, nextWorkflowStepId, settings, workflowSteps, updatedAt)
36899
- VALUES (1, ?, ?, ?, ?, ?)`
37340
+ `INSERT INTO config (id, nextWorkflowStepId, settings, workflowSteps, updatedAt)
37341
+ VALUES (1, ?, ?, ?, ?)
37342
+ ON CONFLICT(id) DO UPDATE SET
37343
+ nextWorkflowStepId = excluded.nextWorkflowStepId,
37344
+ settings = excluded.settings,
37345
+ workflowSteps = excluded.workflowSteps,
37346
+ updatedAt = excluded.updatedAt`
36900
37347
  ).run(
36901
- config.nextId || 1,
36902
37348
  nextWorkflowStepId,
36903
37349
  JSON.stringify(config.settings || {}),
36904
37350
  workflowStepsJson,
@@ -36907,7 +37353,7 @@ ${outcome}`;
36907
37353
  this.db.bumpLastModified();
36908
37354
  try {
36909
37355
  const tmpPath = this.configPath + ".tmp";
36910
- await writeFile6(tmpPath, JSON.stringify(config, null, 2));
37356
+ await writeFile6(tmpPath, this.serializeConfigForDisk(config));
36911
37357
  await rename4(tmpPath, this.configPath);
36912
37358
  } catch (err) {
36913
37359
  storeLog.warn("Backward-compat config.json sync failed after config write", {
@@ -37154,9 +37600,7 @@ ${outcome}`;
37154
37600
  if (input.dependencies?.includes(id)) {
37155
37601
  throw new Error(`Task ${id} cannot depend on itself`);
37156
37602
  }
37157
- if (this.readTaskFromDb(id)) {
37158
- throw new Error(`Task ID already exists: ${id}`);
37159
- }
37603
+ this.assertTaskIdAvailable(id);
37160
37604
  const title = input.title?.trim() || void 0;
37161
37605
  let resolvedWorkflowSteps = input.enabledWorkflowSteps?.length ? await this.resolveEnabledWorkflowSteps(input.enabledWorkflowSteps) : void 0;
37162
37606
  if (input.enabledWorkflowSteps === void 0 && options.applyDefaultWorkflowSteps !== false) {
@@ -37252,9 +37696,9 @@ ${outcome}`;
37252
37696
  createdAt: now,
37253
37697
  updatedAt: options?.updatedAt ?? now
37254
37698
  };
37699
+ this.assertTaskIdAvailable(id);
37255
37700
  const dir2 = this.taskDir(id);
37256
- await mkdir7(dir2, { recursive: true });
37257
- await this.atomicWriteTaskJson(dir2, task);
37701
+ await this.atomicCreateTaskJson(dir2, task, "createTask");
37258
37702
  if (this.isWatching) this.taskCache.set(id, { ...task });
37259
37703
  const prompt = options?.promptOverride ?? (task.column === "triage" ? buildBootstrapPrompt(id, task.title, task.description) : this.generateSpecifiedPrompt(task));
37260
37704
  await mkdir7(dir2, { recursive: true });
@@ -37293,9 +37737,9 @@ ${outcome}`;
37293
37737
  updatedAt: now,
37294
37738
  baseBranch: sourceTask.baseBranch
37295
37739
  };
37740
+ this.assertTaskIdAvailable(newId);
37296
37741
  const newDir = this.taskDir(newId);
37297
- await mkdir7(newDir, { recursive: true });
37298
- await this.atomicWriteTaskJson(newDir, newTask);
37742
+ await this.atomicCreateTaskJson(newDir, newTask, "duplicateTask");
37299
37743
  await mkdir7(newDir, { recursive: true });
37300
37744
  await writeFile6(join16(newDir, "PROMPT.md"), sourceTask.prompt);
37301
37745
  if (this.isWatching) this.taskCache.set(newId, { ...newTask });
@@ -37349,9 +37793,9 @@ Refines: ${id}`,
37349
37793
  updatedAt: now,
37350
37794
  attachments: sourceTask.attachments ? [...sourceTask.attachments] : void 0
37351
37795
  };
37796
+ this.assertTaskIdAvailable(newId);
37352
37797
  const newDir = this.taskDir(newId);
37353
- await mkdir7(newDir, { recursive: true });
37354
- await this.atomicWriteTaskJson(newDir, newTask);
37798
+ await this.atomicCreateTaskJson(newDir, newTask, "refineTask");
37355
37799
  const prompt = `# ${newTask.title}
37356
37800
 
37357
37801
  ${newTask.description}
@@ -37733,6 +38177,8 @@ ${newTask.description}
37733
38177
  task.status = void 0;
37734
38178
  task.error = void 0;
37735
38179
  task.blockedBy = void 0;
38180
+ task.paused = void 0;
38181
+ task.pausedByAgentId = void 0;
37736
38182
  const hasNonPendingStepProgress = task.steps.some((step) => step.status !== "pending");
37737
38183
  const preserveStepProgress = options?.preserveResumeState || options?.preserveProgress === true && hasNonPendingStepProgress;
37738
38184
  if (!options?.preserveWorktree) {
@@ -38404,21 +38850,23 @@ ${newTask.description}
38404
38850
  target: input.target,
38405
38851
  metadata: input.metadata
38406
38852
  };
38407
- this.db.prepare(`
38408
- INSERT INTO runAuditEvents (
38409
- id, timestamp, taskId, agentId, runId, domain, mutationType, target, metadata
38410
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
38411
- `).run(
38412
- event.id,
38413
- event.timestamp,
38414
- event.taskId ?? null,
38415
- event.agentId,
38416
- event.runId,
38417
- event.domain,
38418
- event.mutationType,
38419
- event.target,
38420
- toJsonNullable(event.metadata)
38421
- );
38853
+ this.db.transactionImmediate(() => {
38854
+ this.db.prepare(`
38855
+ INSERT INTO runAuditEvents (
38856
+ id, timestamp, taskId, agentId, runId, domain, mutationType, target, metadata
38857
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
38858
+ `).run(
38859
+ event.id,
38860
+ event.timestamp,
38861
+ event.taskId ?? null,
38862
+ event.agentId,
38863
+ event.runId,
38864
+ event.domain,
38865
+ event.mutationType,
38866
+ event.target,
38867
+ toJsonNullable(event.metadata)
38868
+ );
38869
+ });
38422
38870
  return event;
38423
38871
  }
38424
38872
  /**
@@ -40551,6 +40999,7 @@ ${stepsSection}`;
40551
40999
  if (this._db) {
40552
41000
  this._db.close();
40553
41001
  this._db = null;
41002
+ this.taskIdStateReconciled = false;
40554
41003
  }
40555
41004
  if (this._archiveDb) {
40556
41005
  this._archiveDb.close();
@@ -40584,9 +41033,9 @@ ${stepsSection}`;
40584
41033
  }
40585
41034
  getDatabaseHealth() {
40586
41035
  return {
40587
- corruptionDetected: this.db.corruptionDetected,
40588
- integrityCheckPending: this.db.integrityCheckPending,
40589
- integrityCheckLastRunAt: this.db.integrityCheckLastRunAt
41036
+ healthy: !this.db.corruptionDetected,
41037
+ lastCheckedAt: this.db.integrityCheckLastRunAt ? new Date(this.db.integrityCheckLastRunAt) : null,
41038
+ isRunning: this.db.integrityCheckPending
40590
41039
  };
40591
41040
  }
40592
41041
  getDistributedTaskIdAllocator() {
@@ -41100,7 +41549,7 @@ var init_daemon_token = __esm({
41100
41549
  });
41101
41550
 
41102
41551
  // ../core/src/pi-extensions.ts
41103
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync3, writeFileSync } from "node:fs";
41552
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync4, writeFileSync } from "node:fs";
41104
41553
  import { homedir as homedir3 } from "node:os";
41105
41554
  import { basename as basename5, isAbsolute as isAbsolute5, join as join17, relative as relative2, resolve as resolve8, sep as sep4, win32 } from "node:path";
41106
41555
  function getHomeDir3(home) {
@@ -41190,7 +41639,7 @@ function discoverExtensionsInDir(dir2, cwd, home) {
41190
41639
  let isDirectory = entry.isDirectory();
41191
41640
  if (entry.isSymbolicLink()) {
41192
41641
  try {
41193
- isDirectory = statSync3(entryPath).isDirectory();
41642
+ isDirectory = statSync4(entryPath).isDirectory();
41194
41643
  } catch {
41195
41644
  isDirectory = false;
41196
41645
  }
@@ -49066,11 +49515,11 @@ var require_extract_zip = __commonJS({
49066
49515
  var { createWriteStream: createWriteStream2, promises: fs3 } = __require("fs");
49067
49516
  var getStream = require_get_stream();
49068
49517
  var path6 = __require("path");
49069
- var { promisify: promisify20 } = __require("util");
49518
+ var { promisify: promisify21 } = __require("util");
49070
49519
  var stream = __require("stream");
49071
49520
  var yauzl = require_yauzl();
49072
- var openZip = promisify20(yauzl.open);
49073
- var pipeline = promisify20(stream.pipeline);
49521
+ var openZip = promisify21(yauzl.open);
49522
+ var pipeline = promisify21(stream.pipeline);
49074
49523
  var Extractor = class {
49075
49524
  constructor(zipPath, opts) {
49076
49525
  this.zipPath = zipPath;
@@ -49152,7 +49601,7 @@ var require_extract_zip = __commonJS({
49152
49601
  await fs3.mkdir(destDir, mkdirOptions);
49153
49602
  if (isDir) return;
49154
49603
  debug("opening read stream", dest);
49155
- const readStream = await promisify20(this.zipfile.openReadStream.bind(this.zipfile))(entry);
49604
+ const readStream = await promisify21(this.zipfile.openReadStream.bind(this.zipfile))(entry);
49156
49605
  if (symlink) {
49157
49606
  const link = await getStream(readStream);
49158
49607
  debug("creating symlink", link, dest);
@@ -56498,7 +56947,7 @@ var require_dist3 = __commonJS({
56498
56947
  });
56499
56948
 
56500
56949
  // ../core/src/agent-companies-parser.ts
56501
- import { existsSync as existsSync19, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync, statSync as statSync4 } from "node:fs";
56950
+ import { existsSync as existsSync19, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync, statSync as statSync5 } from "node:fs";
56502
56951
  import { tmpdir as tmpdir3 } from "node:os";
56503
56952
  import { isAbsolute as isAbsolute7, join as join22, normalize as normalize3, resolve as resolve11 } from "node:path";
56504
56953
  function slugifyAgentReference(value) {
@@ -56774,7 +57223,7 @@ function parseCompanyDirectory(dirPath) {
56774
57223
  if (!existsSync19(resolvedPath)) {
56775
57224
  throw new AgentCompaniesParseError(`Company directory does not exist: ${resolvedPath}`);
56776
57225
  }
56777
- if (!statSync4(resolvedPath).isDirectory()) {
57226
+ if (!statSync5(resolvedPath).isDirectory()) {
56778
57227
  throw new AgentCompaniesParseError(`Company path is not a directory: ${resolvedPath}`);
56779
57228
  }
56780
57229
  const companyPath = join22(resolvedPath, "COMPANY.md");
@@ -56813,12 +57262,12 @@ function resolveExtractionRoot(tempDir) {
56813
57262
  return tempDir;
56814
57263
  }
56815
57264
  async function extractTarArchive(archivePath, outputDir) {
56816
- const [{ execFile: execFile11 }, { promisify: promisify20 }] = await Promise.all([
57265
+ const [{ execFile: execFile12 }, { promisify: promisify21 }] = await Promise.all([
56817
57266
  import("node:child_process"),
56818
57267
  import("node:util")
56819
57268
  ]);
56820
- const execFileAsync8 = promisify20(execFile11);
56821
- await execFileAsync8("tar", ["xzf", archivePath, "-C", outputDir]);
57269
+ const execFileAsync9 = promisify21(execFile12);
57270
+ await execFileAsync9("tar", ["xzf", archivePath, "-C", outputDir]);
56822
57271
  }
56823
57272
  function sanitizeCompanySubPath(subPath) {
56824
57273
  const trimmed = subPath.trim();
@@ -62373,15 +62822,15 @@ function evaluateAgentActionGate(params) {
62373
62822
  let resourceId;
62374
62823
  if (params.toolName === "bash") {
62375
62824
  const command = extractShellCommand(args);
62376
- const git = classifyGitCommand(command);
62377
- if (git?.write) {
62825
+ const git2 = classifyGitCommand(command);
62826
+ if (git2?.write) {
62378
62827
  category = "git_write";
62379
- operation = git.operation;
62828
+ operation = git2.operation;
62380
62829
  resourceType = "git";
62381
62830
  } else {
62382
62831
  category = "command_execution";
62383
- operation = git?.operation ?? "shell command";
62384
- resourceType = git ? "git" : "command";
62832
+ operation = git2?.operation ?? "shell command";
62833
+ resourceType = git2 ? "git" : "command";
62385
62834
  }
62386
62835
  } else if (params.toolName === "write" || params.toolName === "edit") {
62387
62836
  category = "file_write_delete";
@@ -64240,6 +64689,71 @@ _... existing specification middle trimmed ..._
64240
64689
 
64241
64690
  ${tail}`;
64242
64691
  }
64692
+ function extractMarkdownSection(document2, headingName) {
64693
+ const heading = `## ${headingName}`;
64694
+ const start = document2.indexOf(heading);
64695
+ if (start === -1) {
64696
+ return "";
64697
+ }
64698
+ const afterHeading = start + heading.length;
64699
+ const nextH2 = document2.indexOf("\n## ", afterHeading);
64700
+ const nextH1 = document2.indexOf("\n# ", afterHeading);
64701
+ const endCandidates = [nextH2, nextH1].filter((value) => value !== -1);
64702
+ const end = endCandidates.length > 0 ? Math.min(...endCandidates) : document2.length;
64703
+ return document2.slice(start, end).trim();
64704
+ }
64705
+ function compactTaskPromptStepsSection(section) {
64706
+ const stepTitles = Array.from(section.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
64707
+ if (stepTitles.length === 0) {
64708
+ return section.trim();
64709
+ }
64710
+ return [
64711
+ "## Steps",
64712
+ ...stepTitles,
64713
+ "",
64714
+ "_... step checklist details trimmed for context limits ..._"
64715
+ ].join("\n").trim();
64716
+ }
64717
+ function truncateCompactedSection(section, maxChars, label) {
64718
+ const trimmed = section.trim();
64719
+ if (!trimmed || trimmed.length <= maxChars) {
64720
+ return trimmed;
64721
+ }
64722
+ const marker = `_... ${label} trimmed for context limits ..._`;
64723
+ const headBudget = Math.max(200, maxChars - marker.length - 2);
64724
+ return [
64725
+ `${trimmed.slice(0, headBudget).trimEnd()}\u2026`,
64726
+ "",
64727
+ marker
64728
+ ].join("\n").trim();
64729
+ }
64730
+ function compactTaskPromptSectionBody(body) {
64731
+ const trimmed = body.trim();
64732
+ if (trimmed.length <= MAX_COMPACTED_TASK_PROMPT_CHARS) {
64733
+ return trimmed;
64734
+ }
64735
+ const fencedMatch = /^```markdown\s*\n([\s\S]*?)\n```$/m.exec(trimmed);
64736
+ const promptContent = fencedMatch ? fencedMatch[1].trim() : trimmed;
64737
+ const firstSectionIndex = promptContent.indexOf("\n## ");
64738
+ const preamble = (firstSectionIndex === -1 ? promptContent : promptContent.slice(0, firstSectionIndex)).trim();
64739
+ const missionSection = extractMarkdownSection(promptContent, "Mission");
64740
+ const dependenciesSection = extractMarkdownSection(promptContent, "Dependencies");
64741
+ const fileScopeSection = extractMarkdownSection(promptContent, "File Scope");
64742
+ const stepsSection = compactTaskPromptStepsSection(extractMarkdownSection(promptContent, "Steps"));
64743
+ const compactedContent = [
64744
+ truncateCompactedSection(preamble, 400, "task header"),
64745
+ truncateCompactedSection(missionSection, 900, "mission"),
64746
+ truncateCompactedSection(dependenciesSection, 500, "dependencies"),
64747
+ truncateCompactedSection(fileScopeSection, 1e3, "file scope"),
64748
+ truncateCompactedSection(stepsSection, 1200, "steps outline"),
64749
+ "_... remaining PROMPT.md sections trimmed for context limits ..._"
64750
+ ].filter(Boolean).join("\n\n").trim();
64751
+ const narrowedContent = compactedContent.length <= MAX_COMPACTED_TASK_PROMPT_CHARS ? compactedContent : compactExistingSpecificationSectionBody(compactedContent);
64752
+ const finalContent = fencedMatch ? `\`\`\`markdown
64753
+ ${narrowedContent}
64754
+ \`\`\`` : narrowedContent;
64755
+ return finalContent.length < trimmed.length ? finalContent : compactExistingSpecificationSectionBody(trimmed);
64756
+ }
64243
64757
  function compactUserCommentsSectionBody(body) {
64244
64758
  const trimmed = body.trim();
64245
64759
  if (trimmed.length <= MAX_COMPACTED_USER_COMMENTS_CHARS) {
@@ -64272,7 +64786,7 @@ function compactUserCommentsSectionBody(body) {
64272
64786
  ].join("\n").trim();
64273
64787
  }
64274
64788
  function compactLargePromptSections(prompt) {
64275
- const sectionPattern = /(^|\n)(## (?:Subtask Consideration|Subtask Breakdown Requested|Attachments|Existing Specification|User Comments)\n)([\s\S]*?)(?=\n## [^#]|\n# [^#]|$)/g;
64789
+ const sectionPattern = /(^|\n)(## (?:Subtask Consideration|Subtask Breakdown Requested|Attachments|Existing Specification|Task PROMPT\.md|User Comments)\n)((?:\n*```markdown[\s\S]*?\n```|[\s\S]*?))(?=\n## [^#]|\n# [^#]|$)/g;
64276
64790
  let changed = false;
64277
64791
  const compactedPrompt = prompt.replace(sectionPattern, (match, prefix, heading, body) => {
64278
64792
  const headingName = heading.trim().replace(/^##\s+/, "");
@@ -64282,6 +64796,7 @@ function compactLargePromptSections(prompt) {
64282
64796
  "Subtask Breakdown Requested": MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS,
64283
64797
  Attachments: MAX_COMPACTED_ATTACHMENTS_CHARS,
64284
64798
  "Existing Specification": MAX_COMPACTED_EXISTING_SPEC_CHARS,
64799
+ "Task PROMPT.md": MAX_COMPACTED_TASK_PROMPT_CHARS,
64285
64800
  "User Comments": MAX_COMPACTED_USER_COMMENTS_CHARS
64286
64801
  };
64287
64802
  const maxChars = maxByHeading[headingName] ?? MAX_COMPACTED_PROMPT_MEMORY_CHARS;
@@ -64295,6 +64810,8 @@ function compactLargePromptSections(prompt) {
64295
64810
  compactedBody = compactAttachmentSectionBody(trimmedBody);
64296
64811
  } else if (headingName === "Existing Specification") {
64297
64812
  compactedBody = compactExistingSpecificationSectionBody(trimmedBody);
64813
+ } else if (headingName === "Task PROMPT.md") {
64814
+ compactedBody = compactTaskPromptSectionBody(trimmedBody);
64298
64815
  } else if (headingName === "User Comments") {
64299
64816
  compactedBody = compactUserCommentsSectionBody(trimmedBody);
64300
64817
  }
@@ -65319,7 +65836,7 @@ async function createFnAgent2(options) {
65319
65836
  });
65320
65837
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
65321
65838
  }
65322
- var execAsync, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
65839
+ var execAsync, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_TASK_PROMPT_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
65323
65840
  var init_pi = __esm({
65324
65841
  "../engine/src/pi.ts"() {
65325
65842
  "use strict";
@@ -65345,6 +65862,7 @@ var init_pi = __esm({
65345
65862
  MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS = 1200;
65346
65863
  MAX_COMPACTED_ATTACHMENTS_CHARS = 4e3;
65347
65864
  MAX_COMPACTED_EXISTING_SPEC_CHARS = 4e3;
65865
+ MAX_COMPACTED_TASK_PROMPT_CHARS = MAX_COMPACTED_EXISTING_SPEC_CHARS;
65348
65866
  MAX_COMPACTED_USER_COMMENTS_CHARS = 2e3;
65349
65867
  GATE_BYPASS_TOOL_NAMES = /* @__PURE__ */ new Set([
65350
65868
  "fn_heartbeat_done",
@@ -66613,7 +67131,7 @@ var init_research_step_runner = __esm({
66613
67131
  // ../engine/src/agent-tools.ts
66614
67132
  import { appendFile as appendFile3, mkdir as mkdir12, readFile as readFile14, readdir as readdir8, stat as stat5, writeFile as writeFile11 } from "node:fs/promises";
66615
67133
  import { existsSync as existsSync24 } from "node:fs";
66616
- import { createHash as createHash5 } from "node:crypto";
67134
+ import { createHash as createHash6 } from "node:crypto";
66617
67135
  import { join as join30, relative as relative6, resolve as resolve16 } from "node:path";
66618
67136
  import { Type } from "@mariozechner/pi-ai";
66619
67137
  function sanitizeAgentMemoryId(agentId) {
@@ -66656,7 +67174,7 @@ async function readAgentMemoryWorkspaceLongTerm(rootDir, agentId) {
66656
67174
  }
66657
67175
  }
66658
67176
  function qmdAgentMemoryCollectionName2(rootDir, agentId) {
66659
- const hash = createHash5("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
67177
+ const hash = createHash6("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
66660
67178
  return `fusion-agent-memory-${sanitizeAgentMemoryId(agentId).toLowerCase()}-${hash}`;
66661
67179
  }
66662
67180
  function buildQmdAgentMemoryCollectionAddArgs(rootDir, agentId) {
@@ -66785,11 +67303,11 @@ async function refreshAgentMemoryQmdIndex(rootDir, agentMemory) {
66785
67303
  return;
66786
67304
  }
66787
67305
  const promise = (async () => {
66788
- const { execFile: execFile11 } = await import("node:child_process");
66789
- const { promisify: promisify20 } = await import("node:util");
66790
- const execFileAsync8 = promisify20(execFile11);
67306
+ const { execFile: execFile12 } = await import("node:child_process");
67307
+ const { promisify: promisify21 } = await import("node:util");
67308
+ const execFileAsync9 = promisify21(execFile12);
66791
67309
  try {
66792
- await execFileAsync8("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
67310
+ await execFileAsync9("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
66793
67311
  cwd: rootDir,
66794
67312
  timeout: 4e3,
66795
67313
  maxBuffer: 512 * 1024
@@ -66802,8 +67320,8 @@ ${stderr}`)) {
66802
67320
  throw error;
66803
67321
  }
66804
67322
  }
66805
- await execFileAsync8("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
66806
- await execFileAsync8("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
67323
+ await execFileAsync9("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
67324
+ await execFileAsync9("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
66807
67325
  })();
66808
67326
  agentQmdRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
66809
67327
  try {
@@ -66858,10 +67376,10 @@ async function searchAgentMemoryWithQmd(rootDir, agentMemory, query, limit) {
66858
67376
  }
66859
67377
  try {
66860
67378
  await refreshAgentMemoryQmdIndex(rootDir, agentMemory);
66861
- const { execFile: execFile11 } = await import("node:child_process");
66862
- const { promisify: promisify20 } = await import("node:util");
66863
- const execFileAsync8 = promisify20(execFile11);
66864
- const { stdout } = await execFileAsync8("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
67379
+ const { execFile: execFile12 } = await import("node:child_process");
67380
+ const { promisify: promisify21 } = await import("node:util");
67381
+ const execFileAsync9 = promisify21(execFile12);
67382
+ const { stdout } = await execFileAsync9("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
66865
67383
  cwd: rootDir,
66866
67384
  timeout: 4e3,
66867
67385
  maxBuffer: 1024 * 1024
@@ -66940,24 +67458,36 @@ function createTaskCreateTool(store, provenance, options) {
66940
67458
  description: "Create a new task for out-of-scope work discovered during execution. The task goes into triage where it will be specified by the AI. Optionally set dependencies (e.g., the new task depends on the current one, or the current task should wait for the new one).",
66941
67459
  parameters: taskCreateParams,
66942
67460
  execute: async (_id, params) => {
66943
- const task = await createAgentTask(store, {
66944
- description: params.description,
66945
- dependencies: params.dependencies,
66946
- column: "triage",
66947
- source: provenance ? {
66948
- sourceType: provenance.sourceType,
66949
- sourceAgentId: provenance.sourceAgentId,
66950
- sourceRunId: provenance.sourceRunId
66951
- } : void 0
66952
- }, options);
66953
- const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
66954
- return {
66955
- content: [{
66956
- type: "text",
66957
- text: `Created ${task.id}: ${params.description}${deps}`
66958
- }],
66959
- details: { taskId: task.id }
66960
- };
67461
+ try {
67462
+ const task = await createAgentTask(store, {
67463
+ description: params.description,
67464
+ dependencies: params.dependencies,
67465
+ column: "triage",
67466
+ priority: params.priority,
67467
+ source: provenance ? {
67468
+ sourceType: provenance.sourceType,
67469
+ sourceAgentId: provenance.sourceAgentId,
67470
+ sourceRunId: provenance.sourceRunId
67471
+ } : void 0
67472
+ }, options);
67473
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
67474
+ return {
67475
+ content: [{
67476
+ type: "text",
67477
+ text: `Created ${task.id}: ${params.description}${deps}`
67478
+ }],
67479
+ details: { taskId: task.id }
67480
+ };
67481
+ } catch (err) {
67482
+ if (err instanceof Error && err.message.startsWith("Task ID already exists:")) {
67483
+ return {
67484
+ content: [{ type: "text", text: `ERROR: ${err.message}` }],
67485
+ details: {},
67486
+ isError: true
67487
+ };
67488
+ }
67489
+ throw err;
67490
+ }
66961
67491
  }
66962
67492
  };
66963
67493
  }
@@ -67868,24 +68398,35 @@ function createDelegateTaskTool(agentStore, taskStore, options) {
67868
68398
  details: {}
67869
68399
  };
67870
68400
  }
67871
- const task = await createAgentTask(taskStore, {
67872
- description: params.description,
67873
- dependencies: params.dependencies,
67874
- column: "todo",
67875
- assignedAgentId: params.agent_id,
67876
- source: {
67877
- sourceType: "api",
67878
- ...override ? { sourceMetadata: { executorRoleOverride: true } } : {}
68401
+ try {
68402
+ const task = await createAgentTask(taskStore, {
68403
+ description: params.description,
68404
+ dependencies: params.dependencies,
68405
+ column: "todo",
68406
+ assignedAgentId: params.agent_id,
68407
+ source: {
68408
+ sourceType: "api",
68409
+ ...override ? { sourceMetadata: { executorRoleOverride: true } } : {}
68410
+ }
68411
+ }, options);
68412
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
68413
+ return {
68414
+ content: [{
68415
+ type: "text",
68416
+ text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
68417
+ }],
68418
+ details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
68419
+ };
68420
+ } catch (err) {
68421
+ if (err instanceof Error && err.message.startsWith("Task ID already exists:")) {
68422
+ return {
68423
+ content: [{ type: "text", text: `ERROR: ${err.message}` }],
68424
+ details: {},
68425
+ isError: true
68426
+ };
67879
68427
  }
67880
- }, options);
67881
- const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
67882
- return {
67883
- content: [{
67884
- type: "text",
67885
- text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
67886
- }],
67887
- details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
67888
- };
68428
+ throw err;
68429
+ }
67889
68430
  }
67890
68431
  };
67891
68432
  }
@@ -68225,7 +68766,7 @@ ${lines.join("\n")}`
68225
68766
  }
68226
68767
  };
68227
68768
  }
68228
- var taskCreateParams, taskLogParams, taskDocumentWriteParams, taskDocumentReadParams, reflectOnPerformanceParams, readEvaluationsParams, updateIdentityParams, listAgentsParams, delegateTaskParams, getAgentConfigParams, updateAgentConfigParams, createAgentParams, deleteAgentParams, sendMessageParams, readMessagesParams, memorySearchParams, memoryGetParams, webFetchParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, memoryAppendParams, log10, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH, AGENT_MEMORY_ROOT3, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2;
68769
+ var TASK_CREATE_PRIORITY_VALUES, taskCreateParams, taskLogParams, taskDocumentWriteParams, taskDocumentReadParams, reflectOnPerformanceParams, readEvaluationsParams, updateIdentityParams, listAgentsParams, delegateTaskParams, getAgentConfigParams, updateAgentConfigParams, createAgentParams, deleteAgentParams, sendMessageParams, readMessagesParams, memorySearchParams, memoryGetParams, webFetchParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, memoryAppendParams, log10, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH, AGENT_MEMORY_ROOT3, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2;
68229
68770
  var init_agent_tools = __esm({
68230
68771
  "../engine/src/agent-tools.ts"() {
68231
68772
  "use strict";
@@ -68236,10 +68777,16 @@ var init_agent_tools = __esm({
68236
68777
  init_logger2();
68237
68778
  init_web_fetch();
68238
68779
  init_agent_action_gate();
68780
+ TASK_CREATE_PRIORITY_VALUES = ["low", "normal", "high", "urgent"];
68239
68781
  taskCreateParams = Type.Object({
68240
68782
  description: Type.String({ description: "What needs to be done" }),
68241
68783
  dependencies: Type.Optional(
68242
68784
  Type.Array(Type.String(), { description: 'Task IDs this new task depends on (e.g. ["KB-001"])' })
68785
+ ),
68786
+ priority: Type.Optional(
68787
+ Type.Union(TASK_CREATE_PRIORITY_VALUES.map((priority) => Type.Literal(priority)), {
68788
+ description: "Task priority (low, normal, high, urgent)"
68789
+ })
68243
68790
  )
68244
68791
  });
68245
68792
  taskLogParams = Type.Object({
@@ -69477,6 +70024,7 @@ var init_ntfy_provider = __esm({
69477
70024
  );
69478
70025
  const response = await sendNtfyNotificationWithResult({
69479
70026
  ntfyBaseUrl: this.config.ntfyBaseUrl,
70027
+ ntfyAccessToken: this.config.ntfyAccessToken,
69480
70028
  topic: this.config.topic,
69481
70029
  title: content.title,
69482
70030
  message: content.message,
@@ -69781,7 +70329,7 @@ var init_notification_service = __esm({
69781
70329
  handleSettingsUpdated = async (data) => {
69782
70330
  const { settings, previous } = data;
69783
70331
  this.setNotificationsEnabledFromSettings(settings);
69784
- if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
70332
+ if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyAccessToken !== previous.ntfyAccessToken || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
69785
70333
  const wasEnabled = Boolean(previous.ntfyEnabled && previous.ntfyTopic);
69786
70334
  const isEnabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
69787
70335
  await this.syncNtfyProvider(settings);
@@ -69793,6 +70341,8 @@ var init_notification_service = __esm({
69793
70341
  schedulerLog.log("NotificationService ntfy topic updated");
69794
70342
  } else if (settings.ntfyBaseUrl !== previous.ntfyBaseUrl) {
69795
70343
  schedulerLog.log("NotificationService ntfy base URL updated");
70344
+ } else if (settings.ntfyAccessToken !== previous.ntfyAccessToken) {
70345
+ schedulerLog.log("NotificationService ntfy access token updated");
69796
70346
  } else if (settings.ntfyDashboardHost !== previous.ntfyDashboardHost) {
69797
70347
  schedulerLog.log("NotificationService ntfy dashboard host updated");
69798
70348
  } else if (JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
@@ -69821,6 +70371,7 @@ var init_notification_service = __esm({
69821
70371
  await this.ntfyProvider.initialize?.({
69822
70372
  topic: settings.ntfyTopic,
69823
70373
  ntfyBaseUrl: settings.ntfyBaseUrl ?? this.options.ntfyBaseUrl,
70374
+ ntfyAccessToken: settings.ntfyAccessToken,
69824
70375
  dashboardHost: settings.ntfyDashboardHost,
69825
70376
  events: settings.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS],
69826
70377
  projectId: this.options.projectId
@@ -70027,6 +70578,7 @@ function buildNtfyClickUrl(options) {
70027
70578
  }
70028
70579
  async function sendNtfyNotificationWithResult({
70029
70580
  ntfyBaseUrl,
70581
+ ntfyAccessToken,
70030
70582
  topic,
70031
70583
  title,
70032
70584
  message,
@@ -70043,6 +70595,10 @@ async function sendNtfyNotificationWithResult({
70043
70595
  if (clickUrl) {
70044
70596
  headers.Click = clickUrl;
70045
70597
  }
70598
+ const trimmedToken = ntfyAccessToken?.trim();
70599
+ if (trimmedToken) {
70600
+ headers.Authorization = `Bearer ${trimmedToken}`;
70601
+ }
70046
70602
  const resolvedBaseUrl = resolveNtfyBaseUrl(ntfyBaseUrl);
70047
70603
  const response = await fetch(`${resolvedBaseUrl}/${topic}`, {
70048
70604
  method: "POST",
@@ -70358,9 +70914,33 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
70358
70914
  this.reason = reason;
70359
70915
  }
70360
70916
  }
70361
- let session;
70362
- try {
70363
- ({ session } = await createResolvedAgentSession({
70917
+ const activeSessions = /* @__PURE__ */ new Set();
70918
+ let reviewText = "";
70919
+ const endSession = (session2) => {
70920
+ if (!activeSessions.delete(session2)) {
70921
+ return;
70922
+ }
70923
+ session2.dispose();
70924
+ options.onSessionEnded?.(session2);
70925
+ };
70926
+ const buildPauseUnavailableResult = async (reason) => {
70927
+ reviewerLog.log(
70928
+ `${taskId}: ${reviewType} review for Step ${stepNumber} aborted before spawn \u2014 ${reason} active`
70929
+ );
70930
+ if (options.store && options.taskId) {
70931
+ await options.store.logEntry(
70932
+ options.taskId,
70933
+ `${reviewType} review aborted before spawn \u2014 ${reason} active`
70934
+ ).catch(() => void 0);
70935
+ }
70936
+ return {
70937
+ verdict: "UNAVAILABLE",
70938
+ review: `${reason} active \u2014 reviewer not spawned. Stop calling fn_review_* and exit cleanly; the parent task will resume after unpause.`,
70939
+ summary: `Skipped: ${reason}`
70940
+ };
70941
+ };
70942
+ const createReviewerSession = async () => {
70943
+ const { session: session2 } = await createResolvedAgentSession({
70364
70944
  sessionPurpose: "reviewer",
70365
70945
  runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig),
70366
70946
  pluginRunner: options.pluginRunner,
@@ -70378,7 +70958,6 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
70378
70958
  fallbackProvider: validatorFallbackProvider,
70379
70959
  fallbackModelId: validatorFallbackModelId,
70380
70960
  defaultThinkingLevel: options.defaultThinkingLevel,
70381
- // Skill selection: use assigned agent skills if available, otherwise role fallback
70382
70961
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
70383
70962
  taskId: options.taskId,
70384
70963
  taskTitle: options.taskTitle,
@@ -70402,52 +70981,138 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
70402
70981
  throw new ReviewerPauseAbortError(reason);
70403
70982
  }
70404
70983
  }
70405
- }));
70984
+ });
70985
+ const reviewerModelDesc = describeModel(session2);
70986
+ const reviewerModelMarker = `Reviewer using model: ${reviewerModelDesc}`;
70987
+ reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDesc}`);
70988
+ if (options.store && options.taskId) {
70989
+ await options.store.logEntry(options.taskId, reviewerModelMarker);
70990
+ await options.store.appendAgentLog(options.taskId, reviewerModelMarker, "text", void 0, "reviewer").catch(() => void 0);
70991
+ }
70992
+ activeSessions.add(session2);
70993
+ options.onSessionCreated?.(session2);
70994
+ session2.subscribe((event) => {
70995
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
70996
+ reviewText += event.assistantMessageEvent.delta;
70997
+ }
70998
+ });
70999
+ return session2;
71000
+ };
71001
+ const runReviewPrompt = async (session2, prompt) => {
71002
+ await promptWithFallback(session2, prompt);
71003
+ checkSessionError(session2);
71004
+ };
71005
+ let session;
71006
+ try {
71007
+ session = await createReviewerSession();
70406
71008
  } catch (err) {
70407
71009
  if (err instanceof ReviewerPauseAbortError) {
70408
- reviewerLog.log(
70409
- `${taskId}: ${reviewType} review for Step ${stepNumber} aborted before spawn \u2014 ${err.reason} active`
70410
- );
70411
- if (options.store && options.taskId) {
70412
- await options.store.logEntry(
70413
- options.taskId,
70414
- `${reviewType} review aborted before spawn \u2014 ${err.reason} active`
70415
- ).catch(() => void 0);
70416
- }
70417
- return {
70418
- verdict: "UNAVAILABLE",
70419
- review: `${err.reason} active \u2014 reviewer not spawned. Stop calling fn_review_* and exit cleanly; the parent task will resume after unpause.`,
70420
- summary: `Skipped: ${err.reason}`
70421
- };
71010
+ return buildPauseUnavailableResult(err.reason);
70422
71011
  }
70423
71012
  throw err;
70424
71013
  }
70425
- const reviewerModelDesc = describeModel(session);
70426
- const reviewerModelMarker = `Reviewer using model: ${reviewerModelDesc}`;
70427
- reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDesc}`);
70428
- if (options.store && options.taskId) {
70429
- await options.store.logEntry(options.taskId, reviewerModelMarker);
70430
- await options.store.appendAgentLog(options.taskId, reviewerModelMarker, "text", void 0, "reviewer").catch(() => void 0);
70431
- }
70432
- options.onSessionCreated?.(session);
70433
- let reviewText = "";
70434
- session.subscribe((event) => {
70435
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
70436
- reviewText += event.assistantMessageEvent.delta;
70437
- }
70438
- });
70439
71014
  try {
70440
- await promptWithFallback(session, request3);
70441
- checkSessionError(session);
71015
+ try {
71016
+ await runReviewPrompt(session, request3);
71017
+ } catch (err) {
71018
+ const errorMessage = err instanceof Error ? err.message : String(err);
71019
+ if (!isContextLimitError(errorMessage)) {
71020
+ throw err;
71021
+ }
71022
+ const retryLogMessage = reviewType === "code" ? "code review hit context limit \u2014 retrying with compacted request" : `${reviewType} review hit context limit \u2014 retrying with compacted request`;
71023
+ reviewerLog.warn(`${taskId}: ${retryLogMessage}`);
71024
+ if (options.store && options.taskId) {
71025
+ await options.store.logEntry(options.taskId, retryLogMessage).catch(() => void 0);
71026
+ }
71027
+ reviewText = "";
71028
+ const reducedRequest = buildReducedReviewRequest(
71029
+ taskId,
71030
+ stepNumber,
71031
+ stepName,
71032
+ reviewType,
71033
+ promptContent,
71034
+ cwd,
71035
+ baseline
71036
+ );
71037
+ try {
71038
+ await runReviewPrompt(session, reducedRequest);
71039
+ } catch (retryErr) {
71040
+ if (!isReviewerSessionReuseError(retryErr)) {
71041
+ throw retryErr;
71042
+ }
71043
+ endSession(session);
71044
+ try {
71045
+ session = await createReviewerSession();
71046
+ } catch (recreateErr) {
71047
+ if (recreateErr instanceof ReviewerPauseAbortError) {
71048
+ return buildPauseUnavailableResult(recreateErr.reason);
71049
+ }
71050
+ throw recreateErr;
71051
+ }
71052
+ await runReviewPrompt(session, reducedRequest);
71053
+ }
71054
+ }
70442
71055
  } finally {
70443
- if (agentLogger) await agentLogger.flush();
70444
- session.dispose();
70445
- options.onSessionEnded?.(session);
71056
+ if (agentLogger) {
71057
+ await agentLogger.flush();
71058
+ }
71059
+ for (const activeSession of [...activeSessions]) {
71060
+ endSession(activeSession);
71061
+ }
70446
71062
  }
70447
71063
  const verdict = extractVerdict(reviewText);
70448
71064
  const summary = extractSummary(reviewText);
70449
71065
  return { verdict, review: reviewText, summary };
70450
71066
  }
71067
+ function isReviewerSessionReuseError(error) {
71068
+ const message = error instanceof Error ? error.message : String(error);
71069
+ return /prompt is in progress|session (?:is )?(?:closed|disposed|ended)|conversation already active/i.test(message);
71070
+ }
71071
+ function extractPromptSection(promptContent, sectionName) {
71072
+ const heading = `## ${sectionName}`;
71073
+ const start = promptContent.indexOf(heading);
71074
+ if (start === -1) {
71075
+ return "";
71076
+ }
71077
+ const afterHeading = start + heading.length;
71078
+ const nextH2 = promptContent.indexOf("\n## ", afterHeading);
71079
+ const nextH1 = promptContent.indexOf("\n# ", afterHeading);
71080
+ const endCandidates = [nextH2, nextH1].filter((value) => value !== -1);
71081
+ const end = endCandidates.length > 0 ? Math.min(...endCandidates) : promptContent.length;
71082
+ return promptContent.slice(start, end).trim();
71083
+ }
71084
+ function summarizePromptSteps(promptContent) {
71085
+ const stepTitles = Array.from(promptContent.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
71086
+ if (stepTitles.length === 0) {
71087
+ return "";
71088
+ }
71089
+ return ["## Steps", ...stepTitles].join("\n");
71090
+ }
71091
+ function buildReducedTaskPromptSummary(promptContent) {
71092
+ const firstSectionIndex = promptContent.indexOf("\n## ");
71093
+ const header = (firstSectionIndex === -1 ? promptContent : promptContent.slice(0, firstSectionIndex)).trim();
71094
+ const sections = [
71095
+ header,
71096
+ extractPromptSection(promptContent, "Mission"),
71097
+ extractPromptSection(promptContent, "Dependencies"),
71098
+ extractPromptSection(promptContent, "File Scope"),
71099
+ summarizePromptSteps(promptContent),
71100
+ "_... additional PROMPT.md sections omitted after context-limit retry ..._"
71101
+ ].filter(Boolean);
71102
+ return sections.join("\n\n").trim();
71103
+ }
71104
+ function buildReducedReviewRequest(taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline) {
71105
+ return buildReviewRequest(
71106
+ taskId,
71107
+ stepNumber,
71108
+ stepName,
71109
+ reviewType,
71110
+ buildReducedTaskPromptSummary(promptContent),
71111
+ cwd,
71112
+ baseline,
71113
+ void 0
71114
+ );
71115
+ }
70451
71116
  function buildReviewRequest(taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline, userComments) {
70452
71117
  const parts = [
70453
71118
  `Review request for task ${taskId}, Step ${stepNumber}: ${stepName}`,
@@ -70572,6 +71237,7 @@ var init_reviewer = __esm({
70572
71237
  "use strict";
70573
71238
  init_src();
70574
71239
  init_pi();
71240
+ init_context_limit_detector();
70575
71241
  init_agent_session_helpers();
70576
71242
  init_session_skill_context();
70577
71243
  init_agent_logger();
@@ -72548,11 +73214,17 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
72548
73214
  const taskGetParams = Type2.Object({
72549
73215
  id: Type2.String({ description: "Task ID (e.g. KB-001)" })
72550
73216
  });
73217
+ const taskCreatePriorityValues = ["low", "normal", "high", "urgent"];
72551
73218
  const taskCreateParams3 = Type2.Object({
72552
73219
  title: Type2.Optional(Type2.String({ description: "Short child task title" })),
72553
73220
  description: Type2.String({ description: "Child task description/mission" }),
72554
73221
  dependencies: Type2.Optional(
72555
73222
  Type2.Array(Type2.String({ description: "Task ID dependency (e.g. KB-001)" }))
73223
+ ),
73224
+ priority: Type2.Optional(
73225
+ Type2.Union(taskCreatePriorityValues.map((priority) => Type2.Literal(priority)), {
73226
+ description: "Task priority (low, normal, high, urgent)"
73227
+ })
72556
73228
  )
72557
73229
  });
72558
73230
  const taskList = {
@@ -72674,6 +73346,7 @@ Remove or replace these ids and call fn_task_create again.`
72674
73346
  description: params.description,
72675
73347
  dependencies: validDeps,
72676
73348
  column: "triage",
73349
+ priority: params.priority,
72677
73350
  // Inherit parent's model settings if available
72678
73351
  modelProvider: parentTask?.modelProvider,
72679
73352
  modelId: parentTask?.modelId,
@@ -73489,11 +74162,146 @@ var init_run_audit = __esm({
73489
74162
  }
73490
74163
  });
73491
74164
 
73492
- // ../engine/src/merger.ts
73493
- import { execSync, exec as exec3, execFile as execFile3 } from "node:child_process";
74165
+ // ../engine/src/merger-squash-audit.ts
74166
+ import { execFile as execFile3 } from "node:child_process";
73494
74167
  import { promisify as promisify4 } from "node:util";
74168
+ async function auditSquashMerge({
74169
+ rootDir,
74170
+ squashSha,
74171
+ lookback = DEFAULT_LOOKBACK
74172
+ }) {
74173
+ const normalizedLookback = normalizeLookback(lookback);
74174
+ const parentSha = await git(rootDir, ["rev-parse", `${squashSha}^`]);
74175
+ const squashSubject = await git(rootDir, ["log", "-1", "--format=%s", squashSha]);
74176
+ const branchSubjects = normalizeLines(await git(rootDir, ["log", "-1", "--format=%b", squashSha])).map((line) => line.replace(/^- /, "").trim()).filter(Boolean);
74177
+ const recentMainCommits = await listRecentMainCommits(rootDir, parentSha, normalizedLookback);
74178
+ const recentMainSubjects = recentMainCommits.map((entry) => entry.subject);
74179
+ const duplicateSubjects = branchSubjects.filter((subject) => recentMainSubjects.includes(subject)).map((subject) => ({ type: "duplicate-subject", subject }));
74180
+ const touchedFiles = normalizeLines(await git(rootDir, ["diff", "--name-only", parentSha, squashSha]));
74181
+ const touchedFileOverlaps = [];
74182
+ for (const file of touchedFiles) {
74183
+ const overlappingCommits = [];
74184
+ for (const commit of recentMainCommits) {
74185
+ const touchedInCommit = await git(rootDir, ["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha, "--", file]);
74186
+ if (normalizeLines(touchedInCommit).includes(file)) {
74187
+ overlappingCommits.push({ sha: commit.shortSha, subject: commit.subject });
74188
+ }
74189
+ }
74190
+ if (overlappingCommits.length > 0) {
74191
+ touchedFileOverlaps.push({
74192
+ type: "touched-file-overlap",
74193
+ file,
74194
+ recentMainCommits: overlappingCommits
74195
+ });
74196
+ }
74197
+ }
74198
+ const findings = [...duplicateSubjects, ...touchedFileOverlaps];
74199
+ return {
74200
+ squashSha,
74201
+ parentSha,
74202
+ squashSubject,
74203
+ lookback: normalizedLookback,
74204
+ branchSubjects,
74205
+ recentMainSubjects,
74206
+ duplicateSubjects,
74207
+ touchedFiles,
74208
+ touchedFileOverlaps,
74209
+ findings,
74210
+ issueCount: findings.length,
74211
+ clean: findings.length === 0
74212
+ };
74213
+ }
74214
+ function formatSquashAuditReport(findings) {
74215
+ const lines = [
74216
+ `Auditing squash: ${findings.squashSha} \u2014 ${findings.squashSubject}`,
74217
+ `Parent (main before squash): ${findings.parentSha}`,
74218
+ `Lookback window on main: ${findings.lookback} commits`,
74219
+ "",
74220
+ "=== Duplicate-cherry-pick risk ==="
74221
+ ];
74222
+ if (findings.duplicateSubjects.length === 0) {
74223
+ lines.push("(none \u2014 no branch commit subjects match recent main commits)", "");
74224
+ } else {
74225
+ lines.push(
74226
+ "WARN: branch contains commits whose subjects match recent main commits.",
74227
+ "Auto-resolve may have picked the older side, dropping refinements.",
74228
+ "Action: diff each main commit below against HEAD and confirm its",
74229
+ "net contribution survived. Restore anything dropped as a follow-up.",
74230
+ "",
74231
+ ...findings.duplicateSubjects.map((entry) => ` - ${entry.subject}`),
74232
+ ""
74233
+ );
74234
+ }
74235
+ lines.push(`=== Touched-file overlap (${findings.touchedFiles.length} files in squash) ===`);
74236
+ if (findings.touchedFileOverlaps.length === 0) {
74237
+ lines.push("(none \u2014 squash touches files no recent main commit touched)", "");
74238
+ } else {
74239
+ lines.push(
74240
+ "Files the squash touched that also have recent main activity.",
74241
+ "Action: for each commit below, verify its changes still appear",
74242
+ "in HEAD. Reapply any silently dropped changes on the same branch.",
74243
+ ""
74244
+ );
74245
+ for (const overlap of findings.touchedFileOverlaps) {
74246
+ lines.push(` ${overlap.file}`);
74247
+ for (const commit of overlap.recentMainCommits) {
74248
+ lines.push(` - ${commit.sha} ${commit.subject}`);
74249
+ }
74250
+ }
74251
+ lines.push("");
74252
+ }
74253
+ lines.push(`Audit complete. ${findings.issueCount} item(s) for the calling agent to review.`);
74254
+ return lines.join("\n");
74255
+ }
74256
+ async function git(rootDir, args) {
74257
+ const { stdout } = await execFileAsync("git", args, {
74258
+ cwd: rootDir,
74259
+ encoding: "utf-8",
74260
+ maxBuffer: GIT_OUTPUT_MAX_BUFFER
74261
+ });
74262
+ return stdout.trim();
74263
+ }
74264
+ function normalizeLines(value) {
74265
+ const trimmed = value.trim();
74266
+ if (!trimmed) return [];
74267
+ return trimmed.split("\n").map((line) => line.trim()).filter(Boolean);
74268
+ }
74269
+ async function listRecentMainCommits(rootDir, parentSha, lookback) {
74270
+ const entries = normalizeLines(await git(rootDir, ["log", `--format=%H~%h~%s`, `-n`, String(lookback), parentSha]));
74271
+ return entries.map((entry) => {
74272
+ const [sha, shortSha, ...subjectParts] = entry.split("~");
74273
+ const subject = subjectParts.join("~").trim();
74274
+ if (!sha?.trim() || !shortSha?.trim() || !subject) {
74275
+ return null;
74276
+ }
74277
+ return {
74278
+ sha: sha.trim(),
74279
+ shortSha: shortSha.trim(),
74280
+ subject
74281
+ };
74282
+ }).filter((entry) => entry !== null);
74283
+ }
74284
+ function normalizeLookback(value) {
74285
+ if (!Number.isFinite(value) || !value || value < 1) {
74286
+ return DEFAULT_LOOKBACK;
74287
+ }
74288
+ return Math.trunc(value);
74289
+ }
74290
+ var execFileAsync, DEFAULT_LOOKBACK, GIT_OUTPUT_MAX_BUFFER;
74291
+ var init_merger_squash_audit = __esm({
74292
+ "../engine/src/merger-squash-audit.ts"() {
74293
+ "use strict";
74294
+ execFileAsync = promisify4(execFile3);
74295
+ DEFAULT_LOOKBACK = 30;
74296
+ GIT_OUTPUT_MAX_BUFFER = 10 * 1024 * 1024;
74297
+ }
74298
+ });
74299
+
74300
+ // ../engine/src/merger.ts
74301
+ import { execSync, exec as exec3, execFile as execFile4 } from "node:child_process";
74302
+ import { promisify as promisify5 } from "node:util";
73495
74303
  import { existsSync as existsSync25, readFileSync as readFileSync12, writeFileSync as writeFileSync2, unlinkSync, renameSync as renameSync2 } from "node:fs";
73496
- import { createHash as createHash6 } from "node:crypto";
74304
+ import { createHash as createHash7 } from "node:crypto";
73497
74305
  import { join as join32 } from "node:path";
73498
74306
  import { hostname } from "node:os";
73499
74307
  import { Type as Type3 } from "typebox";
@@ -73562,7 +74370,7 @@ function computeLockfileHash(rootDir) {
73562
74370
  const p = join32(rootDir, name);
73563
74371
  if (existsSync25(p)) {
73564
74372
  try {
73565
- return createHash6("sha256").update(readFileSync12(p)).digest("hex");
74373
+ return createHash7("sha256").update(readFileSync12(p)).digest("hex");
73566
74374
  } catch {
73567
74375
  return null;
73568
74376
  }
@@ -73620,8 +74428,8 @@ function commitOwnedByTask(taskId, subject, body) {
73620
74428
  async function findOwnedLandedCommitForTask(rootDir, task) {
73621
74429
  const tryHydrate = async (sha) => {
73622
74430
  try {
73623
- await execFileAsync("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { cwd: rootDir });
73624
- const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%H%x1f%s%x1f%b", sha], {
74431
+ await execFileAsync2("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { cwd: rootDir });
74432
+ const { stdout } = await execFileAsync2("git", ["log", "-1", "--format=%H%x1f%s%x1f%b", sha], {
73625
74433
  cwd: rootDir,
73626
74434
  encoding: "utf-8"
73627
74435
  });
@@ -73629,7 +74437,7 @@ async function findOwnedLandedCommitForTask(rootDir, task) {
73629
74437
  if (!resolvedSha || !commitOwnedByTask(task.id, subject, body)) return null;
73630
74438
  const owned = { sha: resolvedSha, subject };
73631
74439
  try {
73632
- const { stdout: statsOut } = await execFileAsync("git", ["show", "--shortstat", "--format=", resolvedSha], {
74440
+ const { stdout: statsOut } = await execFileAsync2("git", ["show", "--shortstat", "--format=", resolvedSha], {
73633
74441
  cwd: rootDir,
73634
74442
  encoding: "utf-8"
73635
74443
  });
@@ -73652,7 +74460,7 @@ async function findOwnedLandedCommitForTask(rootDir, task) {
73652
74460
  ];
73653
74461
  for (const args of searches) {
73654
74462
  try {
73655
- const { stdout } = await execFileAsync("git", args, { cwd: rootDir, encoding: "utf-8" });
74463
+ const { stdout } = await execFileAsync2("git", args, { cwd: rootDir, encoding: "utf-8" });
73656
74464
  const first = stdout.trim().split("\n").find(Boolean);
73657
74465
  if (!first) continue;
73658
74466
  const [sha] = first.split("");
@@ -73715,15 +74523,15 @@ async function snapshotDirtyFiles(rootDir) {
73715
74523
  const paths = /* @__PURE__ */ new Set();
73716
74524
  try {
73717
74525
  const [unstagedOut, stagedOut, porcelainOut] = await Promise.all([
73718
- execFileAsync("git", ["diff", "-z", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
74526
+ execFileAsync2("git", ["diff", "-z", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
73719
74527
  (r) => r.stdout,
73720
74528
  () => ""
73721
74529
  ),
73722
- execFileAsync("git", ["diff", "-z", "--cached", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
74530
+ execFileAsync2("git", ["diff", "-z", "--cached", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
73723
74531
  (r) => r.stdout,
73724
74532
  () => ""
73725
74533
  ),
73726
- execFileAsync("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
74534
+ execFileAsync2("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
73727
74535
  (r) => r.stdout,
73728
74536
  () => ""
73729
74537
  )
@@ -73748,18 +74556,18 @@ async function snapshotDirtyFiles(rootDir) {
73748
74556
  async function gitDirtyFingerprint(rootDir) {
73749
74557
  try {
73750
74558
  const [diffOut, statusOut] = await Promise.all([
73751
- execFileAsync("git", ["diff", "HEAD"], {
74559
+ execFileAsync2("git", ["diff", "HEAD"], {
73752
74560
  cwd: rootDir,
73753
74561
  encoding: "utf-8",
73754
74562
  maxBuffer: 64 * 1024 * 1024
73755
74563
  }).then((r) => r.stdout, () => ""),
73756
- execFileAsync("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
74564
+ execFileAsync2("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
73757
74565
  (r) => r.stdout,
73758
74566
  () => ""
73759
74567
  )
73760
74568
  ]);
73761
74569
  if (!diffOut && !statusOut) return "";
73762
- return createHash6("sha256").update(diffOut).update("\0").update(statusOut).digest("hex");
74570
+ return createHash7("sha256").update(diffOut).update("\0").update(statusOut).digest("hex");
73763
74571
  } catch {
73764
74572
  return "";
73765
74573
  }
@@ -75438,7 +76246,7 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
75438
76246
  encoding: "utf-8"
75439
76247
  });
75440
76248
  const unstaged = new Set(unstagedOut.split("\n").map((l) => l.trim()).filter(Boolean));
75441
- const { stdout: porcelainOut } = await execFileAsync("git", ["status", "-z", "--porcelain"], {
76249
+ const { stdout: porcelainOut } = await execFileAsync2("git", ["status", "-z", "--porcelain"], {
75442
76250
  cwd: rootDir,
75443
76251
  encoding: "utf-8"
75444
76252
  });
@@ -75459,7 +76267,7 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
75459
76267
  }
75460
76268
  }
75461
76269
  if (unstagedToStage.length > 0) {
75462
- await execFileAsync("git", ["add", "--", ...unstagedToStage], { cwd: rootDir });
76270
+ await execFileAsync2("git", ["add", "--", ...unstagedToStage], { cwd: rootDir });
75463
76271
  }
75464
76272
  const untrackedToStage = [];
75465
76273
  for (const p of untracked) {
@@ -75472,7 +76280,7 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
75472
76280
  }
75473
76281
  }
75474
76282
  if (untrackedToStage.length > 0) {
75475
- await execFileAsync("git", ["add", "--", ...untrackedToStage], { cwd: rootDir });
76283
+ await execFileAsync2("git", ["add", "--", ...untrackedToStage], { cwd: rootDir });
75476
76284
  }
75477
76285
  const cap = (arr, n = 20) => arr.length <= n ? arr.join(", ") : `${arr.slice(0, n).join(", ")} ... (+${arr.length - n} more)`;
75478
76286
  mergerLog.log(
@@ -75975,8 +76783,8 @@ async function classifyConflict(filePath, cwd) {
75975
76783
  }
75976
76784
  async function resolveWithOurs(filePath, cwd) {
75977
76785
  try {
75978
- await execFileAsync("git", ["checkout", "--ours", "--", filePath], { cwd });
75979
- await execFileAsync("git", ["add", "--", filePath], { cwd });
76786
+ await execFileAsync2("git", ["checkout", "--ours", "--", filePath], { cwd });
76787
+ await execFileAsync2("git", ["add", "--", filePath], { cwd });
75980
76788
  mergerLog.log(`Auto-resolved ${filePath} using --ours`);
75981
76789
  } catch (error) {
75982
76790
  throw new Error(`Failed to auto-resolve ${filePath} with ours: ${error}`);
@@ -75984,8 +76792,8 @@ async function resolveWithOurs(filePath, cwd) {
75984
76792
  }
75985
76793
  async function resolveWithTheirs(filePath, cwd) {
75986
76794
  try {
75987
- await execFileAsync("git", ["checkout", "--theirs", "--", filePath], { cwd });
75988
- await execFileAsync("git", ["add", "--", filePath], { cwd });
76795
+ await execFileAsync2("git", ["checkout", "--theirs", "--", filePath], { cwd });
76796
+ await execFileAsync2("git", ["add", "--", filePath], { cwd });
75989
76797
  mergerLog.log(`Auto-resolved ${filePath} using --theirs`);
75990
76798
  } catch (error) {
75991
76799
  throw new Error(`Failed to auto-resolve ${filePath} with theirs: ${error}`);
@@ -75993,7 +76801,7 @@ async function resolveWithTheirs(filePath, cwd) {
75993
76801
  }
75994
76802
  async function resolveTrivialWhitespace(filePath, cwd) {
75995
76803
  try {
75996
- await execFileAsync("git", ["add", "--", filePath], { cwd });
76804
+ await execFileAsync2("git", ["add", "--", filePath], { cwd });
75997
76805
  mergerLog.log(`Auto-resolved ${filePath} (trivial whitespace)`);
75998
76806
  } catch (error) {
75999
76807
  throw new Error(`Failed to auto-resolve ${filePath} trivial conflict: ${error}`);
@@ -76187,6 +76995,43 @@ async function findWorktreeUser(store, worktreePath, excludeTaskId) {
76187
76995
  function quoteArg(value) {
76188
76996
  return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
76189
76997
  }
76998
+ function shouldRunPostSquashAudit(result, mergeWasEmpty, isEmptyCommit, commitSha) {
76999
+ if (mergeWasEmpty || isEmptyCommit || !commitSha) {
77000
+ return false;
77001
+ }
77002
+ return (result.autoResolvedCount ?? 0) > 0 || result.attemptsMade === 3;
77003
+ }
77004
+ function buildSquashAuditBlockingMessage(taskId, squashSha, findings) {
77005
+ const riskParts = [];
77006
+ if (findings.duplicateSubjects.length > 0) {
77007
+ riskParts.push(`${findings.duplicateSubjects.length} duplicate-subject risk${findings.duplicateSubjects.length === 1 ? "" : "s"}`);
77008
+ }
77009
+ if (findings.touchedFileOverlaps.length > 0) {
77010
+ riskParts.push(`${findings.touchedFileOverlaps.length} touched-file overlap risk${findings.touchedFileOverlaps.length === 1 ? "" : "s"}`);
77011
+ }
77012
+ const summary = riskParts.length > 0 ? riskParts.join(", ") : `${findings.issueCount} audit finding(s)`;
77013
+ return `${taskId}: post-squash audit blocked auto-completion for ${squashSha.slice(0, 8)} (${summary})`;
77014
+ }
77015
+ function formatSquashAuditAgentLog(findings) {
77016
+ const lines = [];
77017
+ if (findings.duplicateSubjects.length > 0) {
77018
+ lines.push("Duplicate-subject risks:");
77019
+ for (const duplicate of findings.duplicateSubjects) {
77020
+ lines.push(`- ${duplicate.subject}`);
77021
+ }
77022
+ }
77023
+ if (findings.touchedFileOverlaps.length > 0) {
77024
+ if (lines.length > 0) lines.push("");
77025
+ lines.push("Touched-file overlap risks:");
77026
+ for (const overlap of findings.touchedFileOverlaps) {
77027
+ lines.push(`- ${overlap.file}`);
77028
+ for (const commit of overlap.recentMainCommits) {
77029
+ lines.push(` - ${commit.sha} ${commit.subject}`);
77030
+ }
77031
+ }
77032
+ }
77033
+ return lines.join("\n");
77034
+ }
76190
77035
  async function resolveSafeCommitBody(opts) {
76191
77036
  const cleanLog = opts.commitLog.trim();
76192
77037
  if (cleanLog.length > 0) return cleanLog;
@@ -77518,6 +78363,26 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
77518
78363
  }
77519
78364
  const isEmptyCommit = filesChanged === 0;
77520
78365
  const recordedSha = isEmptyCommit || mergeWasEmpty ? void 0 : commitSha;
78366
+ const auditSha = recordedSha;
78367
+ if (auditSha && shouldRunPostSquashAudit(result, mergeWasEmpty, isEmptyCommit, auditSha)) {
78368
+ const auditFindings = await auditSquashMerge({
78369
+ rootDir,
78370
+ squashSha: auditSha
78371
+ });
78372
+ if (!auditFindings.clean) {
78373
+ const auditError = new SquashAuditError(taskId, auditSha, auditFindings);
78374
+ await store.appendAgentLog(
78375
+ taskId,
78376
+ auditError.message,
78377
+ "tool_error",
78378
+ formatSquashAuditAgentLog(auditFindings),
78379
+ "merger"
78380
+ );
78381
+ await store.updateTask(taskId, { status: null });
78382
+ throw auditError;
78383
+ }
78384
+ await store.appendAgentLog(taskId, "post-squash audit clean", "text", void 0, "merger");
78385
+ }
77521
78386
  if (isEmptyCommit) {
77522
78387
  mergerLog.warn(
77523
78388
  `${taskId}: local squash produced an empty commit (${commitSha?.slice(0, 8)}) \u2014 branch likely contained dupes of main. Skipping commitSha; recovery will backfill when real commit lands.`
@@ -77582,6 +78447,9 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
77582
78447
  "merger"
77583
78448
  );
77584
78449
  } catch (err) {
78450
+ if (err instanceof SquashAuditError || err?.name === "SquashAuditError") {
78451
+ throw err;
78452
+ }
77585
78453
  mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
77586
78454
  }
77587
78455
  try {
@@ -78817,7 +79685,7 @@ async function completeTask(store, taskId, result) {
78817
79685
  result.task = task;
78818
79686
  store.emit("task:merged", result);
78819
79687
  }
78820
- var execAsync2, execFileAsync, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, INSTALL_MARKER_RELPATH, LOCKFILE_CANDIDATES, VerificationError, MergeAbortedError, VERIFICATION_EXTRA_ENV, AUTOSTASH_LABEL_PREFIX, AUTOSTASH_TIMESTAMP_RE, ACTIVE_MERGER_STATUS_FILENAME, FUSION_TASK_ID_TRAILER_KEY;
79688
+ var execAsync2, execFileAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, INSTALL_MARKER_RELPATH, LOCKFILE_CANDIDATES, VerificationError, MergeAbortedError, SquashAuditError, VERIFICATION_EXTRA_ENV, AUTOSTASH_LABEL_PREFIX, AUTOSTASH_TIMESTAMP_RE, ACTIVE_MERGER_STATUS_FILENAME, FUSION_TASK_ID_TRAILER_KEY;
78821
79689
  var init_merger = __esm({
78822
79690
  "../engine/src/merger.ts"() {
78823
79691
  "use strict";
@@ -78837,8 +79705,9 @@ var init_merger = __esm({
78837
79705
  init_agent_instructions();
78838
79706
  init_run_audit();
78839
79707
  init_agent_tools();
78840
- execAsync2 = promisify4(exec3);
78841
- execFileAsync = promisify4(execFile3);
79708
+ init_merger_squash_audit();
79709
+ execAsync2 = promisify5(exec3);
79710
+ execFileAsync2 = promisify5(execFile4);
78842
79711
  LOCKFILE_PATTERNS = [
78843
79712
  "package-lock.json",
78844
79713
  "pnpm-lock.yaml",
@@ -78895,6 +79764,14 @@ var init_merger = __esm({
78895
79764
  this.name = "MergeAbortedError";
78896
79765
  }
78897
79766
  };
79767
+ SquashAuditError = class extends Error {
79768
+ constructor(taskId, squashSha, findings) {
79769
+ super(buildSquashAuditBlockingMessage(taskId, squashSha, findings));
79770
+ this.squashSha = squashSha;
79771
+ this.findings = findings;
79772
+ this.name = "SquashAuditError";
79773
+ }
79774
+ };
78898
79775
  VERIFICATION_EXTRA_ENV = Object.fromEntries(
78899
79776
  [
78900
79777
  ["FUSION_TEST_TOTAL_WORKERS", "8"],
@@ -79096,7 +79973,7 @@ __export(worktree_pool_exports, {
79096
79973
  scanOrphanedBranches: () => scanOrphanedBranches
79097
79974
  });
79098
79975
  import { exec as exec4 } from "node:child_process";
79099
- import { promisify as promisify5 } from "node:util";
79976
+ import { promisify as promisify6 } from "node:util";
79100
79977
  import { existsSync as existsSync27, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
79101
79978
  import { join as join34, relative as relative8, resolve as resolve18, isAbsolute as isAbsolute10 } from "node:path";
79102
79979
  function getExecStdout(result) {
@@ -79307,7 +80184,7 @@ var init_worktree_pool = __esm({
79307
80184
  "../engine/src/worktree-pool.ts"() {
79308
80185
  "use strict";
79309
80186
  init_logger2();
79310
- execAsync3 = promisify5(exec4);
80187
+ execAsync3 = promisify6(exec4);
79311
80188
  WorktreePool = class {
79312
80189
  idle = /* @__PURE__ */ new Set();
79313
80190
  /**
@@ -79499,7 +80376,7 @@ var init_token_cap_detector = __esm({
79499
80376
 
79500
80377
  // ../engine/src/step-session-executor.ts
79501
80378
  import { exec as exec5 } from "node:child_process";
79502
- import { promisify as promisify6 } from "node:util";
80379
+ import { promisify as promisify7 } from "node:util";
79503
80380
  import { existsSync as existsSync28 } from "node:fs";
79504
80381
  import { join as join35 } from "node:path";
79505
80382
  function parseStepFileScopes(prompt) {
@@ -79762,7 +80639,7 @@ var init_step_session_executor = __esm({
79762
80639
  init_context_limit_detector();
79763
80640
  init_usage_limit_detector();
79764
80641
  init_agent_tools();
79765
- execAsync4 = promisify6(exec5);
80642
+ execAsync4 = promisify7(exec5);
79766
80643
  stepExecLog = createLogger2("step-session-executor");
79767
80644
  MAX_STEP_RETRIES = 3;
79768
80645
  RETRY_DELAYS_MS = [1e3, 5e3, 15e3];
@@ -80416,7 +81293,9 @@ async function hydrateWorktreeDb({
80416
81293
  ensureWorktreeSchema(worktreePath);
80417
81294
  }
80418
81295
  srcDb = new DatabaseSync(srcDbPath);
81296
+ srcDb.exec("PRAGMA busy_timeout = 5000");
80419
81297
  dstDb = openWorktreeDbWithRecovery(dstDbPath, worktreePath);
81298
+ dstDb.exec("PRAGMA busy_timeout = 5000");
80420
81299
  dstDb.exec("PRAGMA journal_mode = WAL");
80421
81300
  const srcTaskCols = getColumns(srcDb, "tasks");
80422
81301
  const dstTaskCols = getColumns(dstDb, "tasks");
@@ -80865,12 +81744,120 @@ var init_run_verification_tool = __esm({
80865
81744
 
80866
81745
  // ../engine/src/executor.ts
80867
81746
  import { exec as exec6 } from "node:child_process";
80868
- import { promisify as promisify7 } from "node:util";
81747
+ import { promisify as promisify8 } from "node:util";
80869
81748
  import { delimiter, isAbsolute as isAbsolute12, join as join39, relative as relative9, resolve as resolvePath } from "node:path";
80870
81749
  import { existsSync as existsSync31 } from "node:fs";
80871
81750
  import { readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
80872
81751
  import { Type as Type5 } from "@mariozechner/pi-ai";
80873
81752
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
81753
+ function normalizeWorkflowScopePath(pathValue) {
81754
+ return pathValue.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
81755
+ }
81756
+ function stripTrailingPathPunctuation(pathValue) {
81757
+ return pathValue.replace(/[),.:;!?]+$/g, "");
81758
+ }
81759
+ function extractReferencedPathsFromWorkflowFeedback(feedback) {
81760
+ const extracted = [];
81761
+ const seen = /* @__PURE__ */ new Set();
81762
+ for (const match of feedback.matchAll(WORKFLOW_FEEDBACK_PATH_REGEX)) {
81763
+ const candidate = stripTrailingPathPunctuation(match[1] ?? match[2] ?? "");
81764
+ const normalized = normalizeWorkflowScopePath(candidate);
81765
+ if (!normalized.includes("/") || !normalized) continue;
81766
+ if (seen.has(normalized)) continue;
81767
+ seen.add(normalized);
81768
+ extracted.push(normalized);
81769
+ }
81770
+ return extracted;
81771
+ }
81772
+ function workflowPathMatchesDeclaredScope(filePath, scopePatterns) {
81773
+ const normalizedPath = normalizeWorkflowScopePath(filePath);
81774
+ for (const rawPattern of scopePatterns) {
81775
+ const pattern = normalizeWorkflowScopePath(rawPattern);
81776
+ if (!pattern) continue;
81777
+ if (/\/\*+$/.test(pattern)) {
81778
+ const directory = pattern.replace(/\/\*+$/, "");
81779
+ if (normalizedPath === directory || normalizedPath.startsWith(`${directory}/`)) return true;
81780
+ continue;
81781
+ }
81782
+ if (pattern.endsWith("/")) {
81783
+ if (normalizedPath.startsWith(pattern)) return true;
81784
+ continue;
81785
+ }
81786
+ if (normalizedPath === pattern) return true;
81787
+ }
81788
+ return false;
81789
+ }
81790
+ function partitionWorkflowRevisionFeedback(feedback, declaredFileScope) {
81791
+ const trimmedFeedback = feedback.trim();
81792
+ if (!trimmedFeedback || declaredFileScope.length === 0) {
81793
+ return {
81794
+ inScopeFeedback: trimmedFeedback,
81795
+ outOfScopeFeedback: "",
81796
+ inScopeSegments: trimmedFeedback ? [trimmedFeedback] : [],
81797
+ outOfScopeSegments: [],
81798
+ detectedPaths: extractReferencedPathsFromWorkflowFeedback(trimmedFeedback)
81799
+ };
81800
+ }
81801
+ const segments = trimmedFeedback.split(/\n\s*\n/).map((segment) => segment.trim()).filter(Boolean);
81802
+ const allDetectedPaths = extractReferencedPathsFromWorkflowFeedback(trimmedFeedback);
81803
+ if (allDetectedPaths.length === 0) {
81804
+ return {
81805
+ inScopeFeedback: trimmedFeedback,
81806
+ outOfScopeFeedback: "",
81807
+ inScopeSegments: trimmedFeedback ? [trimmedFeedback] : [],
81808
+ outOfScopeSegments: [],
81809
+ detectedPaths: []
81810
+ };
81811
+ }
81812
+ const inScopeSegments = [];
81813
+ const outOfScopeSegments = [];
81814
+ for (const segment of segments) {
81815
+ const segmentPaths = extractReferencedPathsFromWorkflowFeedback(segment);
81816
+ if (segmentPaths.length === 0) {
81817
+ inScopeSegments.push(segment);
81818
+ continue;
81819
+ }
81820
+ const hasOutOfScopePath = segmentPaths.some((path6) => !workflowPathMatchesDeclaredScope(path6, declaredFileScope));
81821
+ if (hasOutOfScopePath) {
81822
+ outOfScopeSegments.push(segment);
81823
+ } else {
81824
+ inScopeSegments.push(segment);
81825
+ }
81826
+ }
81827
+ return {
81828
+ inScopeFeedback: inScopeSegments.join("\n\n"),
81829
+ outOfScopeFeedback: outOfScopeSegments.join("\n\n"),
81830
+ inScopeSegments,
81831
+ outOfScopeSegments,
81832
+ detectedPaths: allDetectedPaths
81833
+ };
81834
+ }
81835
+ function normalizeWorktreePath(pathValue) {
81836
+ return resolvePath(pathValue).replace(/\\/g, "/").replace(/\/+$/, "");
81837
+ }
81838
+ async function extractPersistedSessionWorktreePath(sessionFile) {
81839
+ try {
81840
+ const content = await readFile17(sessionFile, "utf-8");
81841
+ const matches = content.match(SESSION_WORKTREE_PATH_REGEX) ?? [];
81842
+ if (matches.length === 0) return null;
81843
+ const normalizedCounts = /* @__PURE__ */ new Map();
81844
+ for (const match of matches) {
81845
+ const normalized = normalizeWorktreePath(match);
81846
+ normalizedCounts.set(normalized, (normalizedCounts.get(normalized) ?? 0) + 1);
81847
+ }
81848
+ let best = null;
81849
+ for (const [path6, count] of normalizedCounts.entries()) {
81850
+ if (!best || count > best.count) best = { path: path6, count };
81851
+ }
81852
+ return best?.path ?? null;
81853
+ } catch {
81854
+ return null;
81855
+ }
81856
+ }
81857
+ function isSessionWorktreeCompatible(persistedWorktreePath, currentWorktreePath) {
81858
+ if (!persistedWorktreePath) return true;
81859
+ return persistedWorktreePath === normalizeWorktreePath(currentWorktreePath);
81860
+ }
80874
81861
  function truncateWorkflowScriptOutput2(output) {
80875
81862
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2) return output;
80876
81863
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2} characters ...
@@ -81150,7 +82137,7 @@ function detectReviewHandoffIntent(commentText) {
81150
82137
  ];
81151
82138
  return handoffPhrases.some((phrase) => text.includes(phrase));
81152
82139
  }
81153
- var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
82140
+ var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, WORKFLOW_FEEDBACK_PATH_REGEX, NonRetryableWorktreeError, SESSION_WORKTREE_PATH_REGEX, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
81154
82141
  var init_executor = __esm({
81155
82142
  "../engine/src/executor.ts"() {
81156
82143
  "use strict";
@@ -81187,7 +82174,7 @@ var init_executor = __esm({
81187
82174
  init_fallback_model_observer();
81188
82175
  init_agent_logger();
81189
82176
  init_agent_tools();
81190
- execAsync5 = promisify7(exec6);
82177
+ execAsync5 = promisify8(exec6);
81191
82178
  STEP_STATUSES = ["pending", "in-progress", "done", "skipped"];
81192
82179
  MAX_WORKFLOW_STEP_RETRIES = 3;
81193
82180
  MAX_TASK_DONE_SESSION_RETRIES = 3;
@@ -81195,8 +82182,10 @@ var init_executor = __esm({
81195
82182
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
81196
82183
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
81197
82184
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2 = 4e3;
82185
+ WORKFLOW_FEEDBACK_PATH_REGEX = /`([^`\n]+)`|(?<![A-Za-z0-9_.-])((?:\.\.?\/)?(?:@?[A-Za-z0-9._-]+\/)+[A-Za-z0-9._-]+(?:\.[A-Za-z0-9._-]+)?)/g;
81198
82186
  NonRetryableWorktreeError = class extends Error {
81199
82187
  };
82188
+ SESSION_WORKTREE_PATH_REGEX = /([A-Za-z]:)?[^"'\s]*\.worktrees[\\/][^"'\s]+/g;
81200
82189
  taskUpdateParams = Type5.Object({
81201
82190
  step: Type5.Number({ description: "Step number (1-indexed)" }),
81202
82191
  status: Type5.Union(
@@ -83270,15 +84259,18 @@ ${summary}`,
83270
84259
  }
83271
84260
  if (!workflowResult.allPassed) {
83272
84261
  if (workflowResult.revisionRequested) {
83273
- await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
83274
- return;
83275
- }
83276
- const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
83277
- if (retried) {
84262
+ const rerunScheduled = await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName, settings);
84263
+ if (rerunScheduled) {
84264
+ return;
84265
+ }
84266
+ } else {
84267
+ const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
84268
+ if (retried) {
84269
+ return;
84270
+ }
84271
+ await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
83278
84272
  return;
83279
84273
  }
83280
- await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
83281
- return;
83282
84274
  }
83283
84275
  } else {
83284
84276
  executorLog.log(`${task.id}: fast mode \u2014 skipping pre-merge workflow steps`);
@@ -83534,7 +84526,23 @@ ${summary}`,
83534
84526
  const executorFallbackProvider = settings.fallbackProvider;
83535
84527
  const executorFallbackModelId = settings.fallbackModelId;
83536
84528
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
83537
- const isResuming = !!task.sessionFile && existsSync31(task.sessionFile);
84529
+ let isResuming = !!task.sessionFile && existsSync31(task.sessionFile);
84530
+ if (isResuming) {
84531
+ const persistedWorktreePath = await extractPersistedSessionWorktreePath(task.sessionFile);
84532
+ if (!isSessionWorktreeCompatible(persistedWorktreePath, worktreePath)) {
84533
+ executorLog.warn(
84534
+ `${task.id}: stale sessionFile worktree mismatch (session=${persistedWorktreePath}, task=${worktreePath}); starting fresh session`
84535
+ );
84536
+ await this.store.logEntry(
84537
+ task.id,
84538
+ `Detected stale persisted session metadata (worktree mismatch: ${persistedWorktreePath} vs ${worktreePath}) \u2014 discarded resume state and started fresh session`,
84539
+ void 0,
84540
+ this.currentRunContext
84541
+ );
84542
+ await this.store.updateTask(task.id, { sessionFile: null });
84543
+ isResuming = false;
84544
+ }
84545
+ }
83538
84546
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
83539
84547
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
83540
84548
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -83764,15 +84772,18 @@ ${summary}`,
83764
84772
  }
83765
84773
  if (!workflowResult.allPassed) {
83766
84774
  if (workflowResult.revisionRequested) {
83767
- await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
83768
- return;
83769
- }
83770
- const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
83771
- if (retried) {
84775
+ const rerunScheduled = await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName, settings);
84776
+ if (rerunScheduled) {
84777
+ return;
84778
+ }
84779
+ } else {
84780
+ const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
84781
+ if (retried) {
84782
+ return;
84783
+ }
84784
+ await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
83772
84785
  return;
83773
84786
  }
83774
- await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
83775
- return;
83776
84787
  }
83777
84788
  } else {
83778
84789
  executorLog.log(`${task.id}: fast mode \u2014 skipping pre-merge workflow steps`);
@@ -83932,11 +84943,14 @@ ${summary}`,
83932
84943
  }
83933
84944
  if (!workflowResult.allPassed) {
83934
84945
  if (workflowResult.revisionRequested) {
83935
- await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
84946
+ const rerunScheduled = await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName, settings);
84947
+ if (rerunScheduled) {
84948
+ return;
84949
+ }
84950
+ } else {
84951
+ await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed on retry");
83936
84952
  return;
83937
84953
  }
83938
- await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed on retry");
83939
- return;
83940
84954
  }
83941
84955
  } else {
83942
84956
  executorLog.log(`${task.id}: fast mode \u2014 skipping pre-merge workflow steps`);
@@ -84736,18 +85750,38 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
84736
85750
  * the injected feedback from PROMPT.md and applies an in-place fix rather
84737
85751
  * than redoing any completed step.
84738
85752
  */
84739
- async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
85753
+ async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName, settings) {
84740
85754
  executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
84741
85755
  this.clearCompletedTaskWatchdog(task.id);
85756
+ const shouldForkOnScopeMismatch = settings.workflowRevisionForkOnScopeMismatch !== false;
85757
+ let inScopeFeedback = feedback.trim();
85758
+ let outOfScopeFeedback = "";
85759
+ let followUpTaskId;
85760
+ if (shouldForkOnScopeMismatch) {
85761
+ const declaredFileScope = await this.store.parseFileScopeFromPrompt(task.id).catch(() => []);
85762
+ const partition = partitionWorkflowRevisionFeedback(feedback, declaredFileScope);
85763
+ inScopeFeedback = partition.inScopeFeedback;
85764
+ outOfScopeFeedback = partition.outOfScopeFeedback;
85765
+ if (outOfScopeFeedback) {
85766
+ const followUpTask = await this.createWorkflowRevisionFollowUpTask(task, stepName, outOfScopeFeedback);
85767
+ followUpTaskId = followUpTask.id;
85768
+ }
85769
+ }
85770
+ if (!inScopeFeedback) {
85771
+ await this.store.logEntry(
85772
+ task.id,
85773
+ followUpTaskId ? `Workflow step "${stepName}" requested revision \u2014 feedback forked to follow-up ${followUpTaskId}; original task left unchanged` : `Workflow step "${stepName}" requested revision \u2014 no in-scope feedback detected`,
85774
+ outOfScopeFeedback || feedback,
85775
+ this.currentRunContext
85776
+ );
85777
+ return false;
85778
+ }
84742
85779
  const updatedTask = await this.store.getTask(task.id);
84743
85780
  const reopen = await this.reopenLastStepForRevision(task.id, updatedTask);
84744
85781
  const reopenSummary = reopen ? `re-opening Step ${reopen.index + 1} ("${reopen.name}") for in-place fix` : "no step to re-open (none were completed)";
84745
- await this.store.logEntry(
84746
- task.id,
84747
- `Workflow step "${stepName}" requested revision \u2014 ${reopenSummary}`,
84748
- feedback
84749
- );
84750
- await this.injectWorkflowRevisionInstructions(task, feedback);
85782
+ const logMessage = followUpTaskId ? `Workflow step "${stepName}" requested revision \u2014 split feedback: appended in-scope guidance and forked out-of-scope work to ${followUpTaskId}; ${reopenSummary}` : `Workflow step "${stepName}" requested revision \u2014 feedback appended to original task; ${reopenSummary}`;
85783
+ await this.store.logEntry(task.id, logMessage, inScopeFeedback, this.currentRunContext);
85784
+ await this.injectWorkflowRevisionInstructions(task, inScopeFeedback);
84751
85785
  await this.store.updateTask(task.id, {
84752
85786
  status: null,
84753
85787
  sessionFile: null
@@ -84758,6 +85792,35 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
84758
85792
  worktreePath,
84759
85793
  `${task.id}: revision rerun scheduled \u2014 moved to todo then in-progress`
84760
85794
  );
85795
+ return true;
85796
+ }
85797
+ async createWorkflowRevisionFollowUpTask(task, stepName, feedback) {
85798
+ const title = `${task.id}: workflow follow-up from ${stepName}`;
85799
+ const description = [
85800
+ `Follow-up work forked from workflow revision feedback on ${task.id}.`,
85801
+ "",
85802
+ `Original task: ${task.id}${task.title ? ` \u2014 ${task.title}` : ""}`,
85803
+ `Workflow step: ${stepName}`,
85804
+ "",
85805
+ "This feedback referenced files outside the original task's declared File Scope, so it was forked into a follow-up task instead of mutating the original PROMPT.md.",
85806
+ "",
85807
+ "## Out-of-Scope Workflow Revision Feedback",
85808
+ "",
85809
+ feedback
85810
+ ].join("\n");
85811
+ return this.store.createTask({
85812
+ title,
85813
+ description,
85814
+ dependencies: [task.id],
85815
+ source: {
85816
+ sourceType: "workflow_step",
85817
+ sourceParentTaskId: task.id,
85818
+ sourceMetadata: {
85819
+ workflowStepName: stepName,
85820
+ routing: "scope-mismatch-fork"
85821
+ }
85822
+ }
85823
+ });
84761
85824
  }
84762
85825
  /**
84763
85826
  * Re-open the last non-pending step so a revision/failure handler gives the
@@ -90054,7 +91117,7 @@ Follow this process:
90054
91117
 
90055
91118
  // ../engine/src/agent-heartbeat.ts
90056
91119
  import { Type as Type6 } from "@mariozechner/pi-ai";
90057
- import { createHash as createHash7 } from "node:crypto";
91120
+ import { createHash as createHash8 } from "node:crypto";
90058
91121
  function formatDuration(ms) {
90059
91122
  const totalMinutes = Math.floor(ms / 6e4);
90060
91123
  if (totalMinutes < 1) return "<1m";
@@ -90098,7 +91161,7 @@ function truncatePrompt(text, maxChars) {
90098
91161
  ... (truncated, ${text.length} chars)`;
90099
91162
  }
90100
91163
  function shortContentHash(value) {
90101
- return createHash7("sha256").update(value).digest("hex").slice(0, 8);
91164
+ return createHash8("sha256").update(value).digest("hex").slice(0, 8);
90102
91165
  }
90103
91166
  function buildIdentitySnapshot(args) {
90104
91167
  const { agent, resolvedInstructions, workspaceMemory } = args;
@@ -91950,13 +93013,13 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
91950
93013
  });
91951
93014
  }
91952
93015
  async buildReportsHealthSection(agentId, agentStore) {
91953
- const getReports = agentStore.getAgentsByReportsTo;
91954
- if (typeof getReports !== "function") {
93016
+ const storeWithReports = agentStore;
93017
+ if (typeof storeWithReports.getAgentsByReportsTo !== "function") {
91955
93018
  return null;
91956
93019
  }
91957
93020
  let reports;
91958
93021
  try {
91959
- reports = await getReports(agentId);
93022
+ reports = await storeWithReports.getAgentsByReportsTo(agentId);
91960
93023
  } catch (err) {
91961
93024
  heartbeatLog.warn(`Failed to load reports for ${agentId}: ${err instanceof Error ? err.message : String(err)}`);
91962
93025
  return null;
@@ -94351,7 +95414,7 @@ var init_cron_runner = __esm({
94351
95414
 
94352
95415
  // ../engine/src/routine-runner.ts
94353
95416
  import { exec as exec8 } from "node:child_process";
94354
- import { promisify as promisify8 } from "node:util";
95417
+ import { promisify as promisify9 } from "node:util";
94355
95418
  function truncateOutput3(stdout, stderr) {
94356
95419
  let output = stdout;
94357
95420
  if (stderr) {
@@ -94373,7 +95436,7 @@ var init_routine_runner = __esm({
94373
95436
  init_logger2();
94374
95437
  init_shell_utils();
94375
95438
  log16 = createLogger2("routine-runner");
94376
- execAsync6 = promisify8(exec8);
95439
+ execAsync6 = promisify9(exec8);
94377
95440
  DEFAULT_TIMEOUT_MS8 = 5 * 60 * 1e3;
94378
95441
  MAX_BUFFER2 = 1024 * 1024;
94379
95442
  MAX_OUTPUT_LENGTH2 = 10 * 1024;
@@ -95310,17 +96373,25 @@ function isMissingWorktreeSessionStartFailure(error) {
95310
96373
  if (typeof error !== "string") {
95311
96374
  return false;
95312
96375
  }
95313
- return error.includes("Refusing to start coding agent in missing worktree:");
96376
+ return error.includes(MISSING_WORKTREE_SESSION_PREFIX);
96377
+ }
96378
+ function extractMissingWorktreePathFromSessionStartFailure(error) {
96379
+ if (typeof error !== "string") return null;
96380
+ const idx = error.indexOf(MISSING_WORKTREE_SESSION_PREFIX);
96381
+ if (idx < 0) return null;
96382
+ const pathPart = error.slice(idx + MISSING_WORKTREE_SESSION_PREFIX.length).trim();
96383
+ return pathPart.length > 0 ? pathPart : null;
95314
96384
  }
95315
96385
  function isRecoverableMissingWorktreeReviewFailure(task) {
95316
96386
  return task.column === "in-review" && !task.paused && task.status === "failed" && isMissingWorktreeSessionStartFailure(task.error) && hasStepProgress(task);
95317
96387
  }
95318
- var log17, RestartRecoveryCoordinator;
96388
+ var log17, MISSING_WORKTREE_SESSION_PREFIX, RestartRecoveryCoordinator;
95319
96389
  var init_restart_recovery_coordinator = __esm({
95320
96390
  "../engine/src/restart-recovery-coordinator.ts"() {
95321
96391
  "use strict";
95322
96392
  init_logger2();
95323
96393
  log17 = createLogger2("restart-recovery");
96394
+ MISSING_WORKTREE_SESSION_PREFIX = "Refusing to start coding agent in missing worktree:";
95324
96395
  RestartRecoveryCoordinator = class {
95325
96396
  constructor(store, executor) {
95326
96397
  this.store = store;
@@ -95364,8 +96435,8 @@ var init_restart_recovery_coordinator = __esm({
95364
96435
 
95365
96436
  // ../engine/src/self-healing.ts
95366
96437
  import { exec as exec9, execSync as execSync2 } from "node:child_process";
95367
- import { promisify as promisify9 } from "node:util";
95368
- import { existsSync as existsSync33, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
96438
+ import { promisify as promisify10 } from "node:util";
96439
+ import { existsSync as existsSync33, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync6 } from "node:fs";
95369
96440
  import { isAbsolute as isAbsolute14, join as join41, relative as relative10, resolve as resolve20 } from "node:path";
95370
96441
  function commitOwnedByTask2(taskId, lineageId, subject, body) {
95371
96442
  if (lineageId && body.includes(`Fusion-Task-Lineage: ${lineageId}`)) {
@@ -95458,7 +96529,7 @@ function isNoTaskDoneFailure2(task) {
95458
96529
  function hasStepProgress2(task) {
95459
96530
  return task.steps.some((step) => step.status !== "pending");
95460
96531
  }
95461
- var log18, execAsync7, APPROVED_TRIAGE_RECOVERY_GRACE_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES, NON_TERMINAL_STEP_STATUSES2, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_AUTO_MERGE_RETRIES, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, SelfHealingManager;
96532
+ var log18, execAsync7, APPROVED_TRIAGE_RECOVERY_GRACE_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES, NON_TERMINAL_STEP_STATUSES2, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_AUTO_MERGE_RETRIES, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, SelfHealingManager;
95462
96533
  var init_self_healing = __esm({
95463
96534
  "../engine/src/self-healing.ts"() {
95464
96535
  "use strict";
@@ -95468,7 +96539,7 @@ var init_self_healing = __esm({
95468
96539
  init_restart_recovery_coordinator();
95469
96540
  init_transient_error_detector();
95470
96541
  log18 = createLogger2("self-healing");
95471
- execAsync7 = promisify9(exec9);
96542
+ execAsync7 = promisify10(exec9);
95472
96543
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
95473
96544
  ORPHANED_EXECUTION_RECOVERY_GRACE_MS = 6e4;
95474
96545
  ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
@@ -95484,6 +96555,7 @@ var init_self_healing = __esm({
95484
96555
  ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
95485
96556
  MAX_TASK_DONE_RETRIES = 3;
95486
96557
  MAX_AUTO_MERGE_RETRIES = 3;
96558
+ MAX_STARVATION_DROPS = 3;
95487
96559
  DEADLOCK_RECOVERY_COOLDOWN_MS = 15 * 6e4;
95488
96560
  DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS = 5 * 6e4;
95489
96561
  DURABLE_ERROR_RECOVERY_MAX_RETRIES = 5;
@@ -95506,6 +96578,7 @@ var init_self_healing = __esm({
95506
96578
  settingsListener = null;
95507
96579
  // ── Per-task deadlock recovery cooldown ─────────────────────────────
95508
96580
  deadlockRecoveryCooldown = /* @__PURE__ */ new Map();
96581
+ mergeStarvationDrops = /* @__PURE__ */ new Map();
95509
96582
  // ── Lifecycle ───────────────────────────────────────────────────────
95510
96583
  start() {
95511
96584
  this.settingsListener = ({ settings, previous }) => {
@@ -96229,6 +97302,7 @@ var init_self_healing = __esm({
96229
97302
  try {
96230
97303
  log18.warn(`Clearing stale merge status for ${task.id}: ${previousStatus}`);
96231
97304
  await this.store.updateTask(task.id, { status: null });
97305
+ this.options.clearMergeActive?.(task.id);
96232
97306
  await this.store.logEntry(
96233
97307
  task.id,
96234
97308
  `Auto-recovered: cleared stale '${previousStatus}' status (no active merger)`
@@ -96299,6 +97373,8 @@ var init_self_healing = __esm({
96299
97373
  reason = `blocker ${blockerId} in-review + paused`;
96300
97374
  } else if (blocker.column === "in-review" && blocker.status === "failed" && (blocker.mergeRetries ?? 0) >= MAX_AUTO_MERGE_RETRIES) {
96301
97375
  reason = `blocker ${blockerId} in-review + failed (mergeRetries ${blocker.mergeRetries ?? 0}/${MAX_AUTO_MERGE_RETRIES})`;
97376
+ } else if (blocker.column === "in-review" && blocker.status === "failed" && isMissingWorktreeSessionStartFailure(blocker.error)) {
97377
+ reason = `blocker ${blockerId} in-review + failed (missing-worktree session start)`;
96302
97378
  } else if (task.dependencies.length > 0 && !unresolvedDeps.includes(blockerId)) {
96303
97379
  reason = `blocker ${blockerId} not among unresolved dependencies`;
96304
97380
  }
@@ -96408,7 +97484,7 @@ var init_self_healing = __esm({
96408
97484
  if (settings.globalPause || settings.enginePaused) return 0;
96409
97485
  const tasks = await this.store.listTasks({ column: "in-review", slim: true });
96410
97486
  const mergeable = tasks.filter(
96411
- (t) => t.column === "in-review" && !t.paused && // Exclude transient merge statuses. Active merges should be left alone;
97487
+ (t) => t.column === "in-review" && !t.paused && t.status !== "failed" && // Exclude transient merge statuses. Active merges should be left alone;
96412
97488
  // stale ones are handled by recoverStaleMergingStatus().
96413
97489
  t.status !== "merging" && t.status !== "merging-pr" && Boolean(t.worktree) && t.mergeDetails?.mergeConfirmed !== true && t.mergeDetails?.noOpMerge !== true && !hasTerminalInvalidDoneTransition(t) && // Mirror ProjectEngine.canMergeTask retry gate. If retries are already
96414
97490
  // exhausted, re-enqueueing here is a no-op and each recovery log write
@@ -96417,6 +97493,13 @@ var init_self_healing = __esm({
96417
97493
  // in case updateTask(moveTask) is briefly out-of-order during recovery.
96418
97494
  (t.mergeRetries ?? 0) < MAX_AUTO_MERGE_RETRIES && getTaskMergeBlocker(t) === void 0
96419
97495
  );
97496
+ const inReviewIds = new Set(tasks.map((task) => task.id));
97497
+ const mergeableIds = new Set(mergeable.map((task) => task.id));
97498
+ for (const taskId of [...this.mergeStarvationDrops.keys()]) {
97499
+ if (!inReviewIds.has(taskId) || !mergeableIds.has(taskId)) {
97500
+ this.mergeStarvationDrops.delete(taskId);
97501
+ }
97502
+ }
96420
97503
  if (mergeable.length === 0) return 0;
96421
97504
  log18.warn(`Found ${mergeable.length} mergeable review task(s) stuck in in-review`);
96422
97505
  const enqueueMerge = this.options.enqueueMerge;
@@ -96424,7 +97507,23 @@ var init_self_healing = __esm({
96424
97507
  for (const task of mergeable) {
96425
97508
  try {
96426
97509
  if (enqueueMerge) {
96427
- enqueueMerge(task.id);
97510
+ const queued = enqueueMerge(task.id);
97511
+ if (!queued) {
97512
+ const drops = (this.mergeStarvationDrops.get(task.id) ?? 0) + 1;
97513
+ this.mergeStarvationDrops.set(task.id, drops);
97514
+ log18.warn(
97515
+ `Auto-recovery enqueue dropped for ${task.id} (${drops}/${MAX_STARVATION_DROPS}); engine merge queue rejected re-enqueue`
97516
+ );
97517
+ if (drops >= MAX_STARVATION_DROPS) {
97518
+ const error = `Auto-merge starvation: ${MAX_STARVATION_DROPS} consecutive enqueue attempts were dropped by the engine merge queue; task requires manual intervention.`;
97519
+ await this.store.updateTask(task.id, { status: "failed", error });
97520
+ await this.store.logEntry(task.id, error);
97521
+ this.mergeStarvationDrops.delete(task.id);
97522
+ recovered++;
97523
+ }
97524
+ continue;
97525
+ }
97526
+ this.mergeStarvationDrops.delete(task.id);
96428
97527
  } else {
96429
97528
  await this.store.mergeTask(task.id);
96430
97529
  }
@@ -97474,16 +98573,18 @@ var init_self_healing = __esm({
97474
98573
  for (const task of candidates) {
97475
98574
  try {
97476
98575
  const staleWorktree = task.worktree;
98576
+ const missingWorktreePath = extractMissingWorktreePathFromSessionStartFailure(task.error);
98577
+ const hasMismatchedLiveWorktree = typeof staleWorktree === "string" && staleWorktree.length > 0 && typeof missingWorktreePath === "string" && missingWorktreePath.length > 0 && resolve20(staleWorktree) !== resolve20(missingWorktreePath);
97477
98578
  await this.store.updateTask(task.id, {
97478
98579
  status: null,
97479
98580
  error: null,
97480
- worktree: null,
97481
- branch: null,
98581
+ worktree: hasMismatchedLiveWorktree ? staleWorktree : null,
98582
+ branch: hasMismatchedLiveWorktree ? task.branch ?? null : null,
97482
98583
  sessionFile: null
97483
98584
  });
97484
98585
  await this.store.logEntry(
97485
98586
  task.id,
97486
- `Auto-recovered: retry/verification session targeted missing worktree${staleWorktree ? ` (${staleWorktree})` : ""} \u2014 cleared stale session metadata and requeued to todo`
98587
+ hasMismatchedLiveWorktree ? `Auto-recovered: stale resume referenced missing worktree (${missingWorktreePath}) while live task worktree is ${staleWorktree} \u2014 cleared stale session metadata and requeued to todo` : `Auto-recovered: retry/verification session targeted missing worktree${staleWorktree ? ` (${staleWorktree})` : ""} \u2014 cleared stale session metadata and requeued to todo`
97487
98588
  );
97488
98589
  await this.store.moveTask(task.id, "todo", { preserveProgress: true });
97489
98590
  recovered++;
@@ -97877,7 +98978,7 @@ var init_self_healing = __esm({
97877
98978
  if (idle.length === 0) return;
97878
98979
  const withMtime = idle.map((p) => {
97879
98980
  try {
97880
- return { path: p, mtime: statSync5(p).mtimeMs };
98981
+ return { path: p, mtime: statSync6(p).mtimeMs };
97881
98982
  } catch (err) {
97882
98983
  const errorMessage = err instanceof Error ? err.message : String(err);
97883
98984
  log18.warn(`Failed to read mtime for worktree ${p}: ${errorMessage} \u2014 defaulting mtime to 0`);
@@ -99092,6 +100193,7 @@ var init_in_process_runtime = __esm({
99092
100193
  * before `start()` via `setMergeEnqueuer`.
99093
100194
  */
99094
100195
  mergeEnqueuer;
100196
+ clearMergeActive;
99095
100197
  activeMergeTaskIdProvider;
99096
100198
  /** Tracks whether startup recovery was intentionally deferred due to pause state. */
99097
100199
  startupRecoveryDeferred = false;
@@ -99489,7 +100591,8 @@ var init_in_process_runtime = __esm({
99489
100591
  recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
99490
100592
  getPlanningTaskIds: () => this.triageProcessor?.getProcessingTaskIds() ?? /* @__PURE__ */ new Set(),
99491
100593
  evictStaleTriageProcessing: () => this.triageProcessor?.evictStaleProcessing() ?? /* @__PURE__ */ new Set(),
99492
- enqueueMerge: this.mergeEnqueuer ? (taskId) => this.mergeEnqueuer?.(taskId) : void 0,
100594
+ enqueueMerge: this.mergeEnqueuer ? (taskId) => this.mergeEnqueuer?.(taskId) ?? false : void 0,
100595
+ clearMergeActive: this.clearMergeActive ? (taskId) => this.clearMergeActive?.(taskId) : void 0,
99493
100596
  getActiveMergeTaskId: () => this.activeMergeTaskIdProvider?.() ?? null,
99494
100597
  leaseManager: this.leaseManager,
99495
100598
  hasActiveAgentExecution: (agentId) => this.heartbeatMonitor?.getTrackedAgents().includes(agentId) ?? false,
@@ -99683,6 +100786,9 @@ var init_in_process_runtime = __esm({
99683
100786
  setMergeEnqueuer(enqueueMerge) {
99684
100787
  this.mergeEnqueuer = enqueueMerge;
99685
100788
  }
100789
+ setMergeActiveClearer(clearMergeActive) {
100790
+ this.clearMergeActive = clearMergeActive;
100791
+ }
99686
100792
  setActiveMergeTaskIdProvider(getActiveMergeTaskId) {
99687
100793
  this.activeMergeTaskIdProvider = getActiveMergeTaskId;
99688
100794
  }
@@ -101787,8 +102893,8 @@ var init_provider_adapters = __esm({
101787
102893
 
101788
102894
  // ../engine/src/remote-access/tunnel-process-manager.ts
101789
102895
  import { EventEmitter as EventEmitter24 } from "node:events";
101790
- import { exec as exec10, execFile as execFile4, spawn as spawn5 } from "node:child_process";
101791
- import { promisify as promisify10 } from "node:util";
102896
+ import { exec as exec10, execFile as execFile5, spawn as spawn5 } from "node:child_process";
102897
+ import { promisify as promisify11 } from "node:util";
101792
102898
  function nowIso2() {
101793
102899
  return (/* @__PURE__ */ new Date()).toISOString();
101794
102900
  }
@@ -101828,7 +102934,7 @@ function toStateError(code, err) {
101828
102934
  at: nowIso2()
101829
102935
  };
101830
102936
  }
101831
- var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync2, execAsync8, LineBuffer, TunnelProcessManager;
102937
+ var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync3, execAsync8, LineBuffer, TunnelProcessManager;
101832
102938
  var init_tunnel_process_manager = __esm({
101833
102939
  "../engine/src/remote-access/tunnel-process-manager.ts"() {
101834
102940
  "use strict";
@@ -101836,8 +102942,8 @@ var init_tunnel_process_manager = __esm({
101836
102942
  init_provider_adapters();
101837
102943
  DEFAULT_MAX_LOG_ENTRIES = 400;
101838
102944
  DEFAULT_STOP_TIMEOUT_MS2 = 5e3;
101839
- execFileAsync2 = promisify10(execFile4);
101840
- execAsync8 = promisify10(exec10);
102945
+ execFileAsync3 = promisify11(execFile5);
102946
+ execAsync8 = promisify11(exec10);
101841
102947
  LineBuffer = class {
101842
102948
  pending = "";
101843
102949
  push(chunk) {
@@ -101914,7 +103020,7 @@ var init_tunnel_process_manager = __esm({
101914
103020
  return null;
101915
103021
  }
101916
103022
  try {
101917
- const { stdout } = await execFileAsync2("tailscale", ["status", "--json"], { timeout: 3e3 });
103023
+ const { stdout } = await execFileAsync3("tailscale", ["status", "--json"], { timeout: 3e3 });
101918
103024
  const data = JSON.parse(String(stdout));
101919
103025
  const dnsName = data.Self?.DNSName?.replace(/\.$/, "");
101920
103026
  if (!dnsName) {
@@ -101937,7 +103043,7 @@ var init_tunnel_process_manager = __esm({
101937
103043
  ];
101938
103044
  for (const resetCommand of resetCommands) {
101939
103045
  try {
101940
- await execFileAsync2(resetCommand.command, resetCommand.args, { timeout: 5e3 });
103046
+ await execFileAsync3(resetCommand.command, resetCommand.args, { timeout: 5e3 });
101941
103047
  return;
101942
103048
  } catch {
101943
103049
  }
@@ -102236,8 +103342,8 @@ var init_tunnel_process_manager = __esm({
102236
103342
  });
102237
103343
 
102238
103344
  // ../engine/src/project-engine.ts
102239
- import { execFile as execFile5 } from "node:child_process";
102240
- import { promisify as promisify11 } from "node:util";
103345
+ import { execFile as execFile6 } from "node:child_process";
103346
+ import { promisify as promisify12 } from "node:util";
102241
103347
  function formatErrorDetails(error) {
102242
103348
  if (error instanceof Error) {
102243
103349
  return {
@@ -102252,7 +103358,7 @@ function isInvalidDoneTransitionError(error) {
102252
103358
  const message = error instanceof Error ? error.message : String(error);
102253
103359
  return message.includes("Invalid transition:") && message.includes("\u2192 'done'");
102254
103360
  }
102255
- var execFileAsync3, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
103361
+ var execFileAsync4, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
102256
103362
  var init_project_engine = __esm({
102257
103363
  "../engine/src/project-engine.ts"() {
102258
103364
  "use strict";
@@ -102270,7 +103376,7 @@ var init_project_engine = __esm({
102270
103376
  init_research_orchestrator();
102271
103377
  init_research_step_runner();
102272
103378
  init_tunnel_process_manager();
102273
- execFileAsync3 = promisify11(execFile5);
103379
+ execFileAsync4 = promisify12(execFile6);
102274
103380
  MERGE_HANDOFF_GRACE_MS = 300;
102275
103381
  isRemoteActive = (ra) => ra?.activeProvider != null && (ra.providers[ra.activeProvider]?.enabled ?? false);
102276
103382
  ProjectEngine = class _ProjectEngine {
@@ -102289,7 +103395,10 @@ var init_project_engine = __esm({
102289
103395
  this.activeMergeTaskId = null;
102290
103396
  }
102291
103397
  this.mergeActive.delete(taskId);
102292
- this.internalEnqueueMerge(taskId);
103398
+ return this.internalEnqueueMerge(taskId);
103399
+ });
103400
+ this.runtime.setMergeActiveClearer?.((taskId) => {
103401
+ this.mergeActive.delete(taskId);
102293
103402
  });
102294
103403
  }
102295
103404
  runtime;
@@ -102324,6 +103433,7 @@ var init_project_engine = __esm({
102324
103433
  mergeAbortController = null;
102325
103434
  mergeRetryTimer = null;
102326
103435
  autostashSweepTimer = null;
103436
+ mergeActiveReconcileTimer = null;
102327
103437
  /**
102328
103438
  * Pending manual merge resolvers — keyed by taskId.
102329
103439
  * When `onMerge` is called, the task is enqueued like auto-merge but a
@@ -102519,6 +103629,7 @@ ${detail}`
102519
103629
  this.wireAutostashOrphanRecovery(store);
102520
103630
  await this.startupMergeSweep(store);
102521
103631
  this.scheduleMergeRetry(store);
103632
+ this.scheduleMergeActiveReconciliation(settings.maintenanceIntervalMs ?? 9e5);
102522
103633
  void this.runStaleAutostashSweep(store, "startup");
102523
103634
  this.scheduleStaleAutostashSweep(store);
102524
103635
  this.started = true;
@@ -102544,6 +103655,10 @@ ${detail}`
102544
103655
  clearTimeout(this.autostashSweepTimer);
102545
103656
  this.autostashSweepTimer = null;
102546
103657
  }
103658
+ if (this.mergeActiveReconcileTimer) {
103659
+ clearInterval(this.mergeActiveReconcileTimer);
103660
+ this.mergeActiveReconcileTimer = null;
103661
+ }
102547
103662
  this.mergeAbortController?.abort();
102548
103663
  this.mergeAbortController = null;
102549
103664
  this.activeMergeTaskId = null;
@@ -102765,7 +103880,7 @@ ${detail}`
102765
103880
  * an external `onMerge` callback (e.g. dashboard's createServer call).
102766
103881
  */
102767
103882
  enqueueMerge(taskId) {
102768
- this.internalEnqueueMerge(taskId);
103883
+ return this.internalEnqueueMerge(taskId);
102769
103884
  }
102770
103885
  /**
102771
103886
  * Perform an AI-powered merge for a task, serialized through the merge queue.
@@ -102782,7 +103897,10 @@ ${detail}`
102782
103897
  }
102783
103898
  return new Promise((resolve52, reject2) => {
102784
103899
  this.manualMergeResolvers.set(taskId, { resolve: resolve52, reject: reject2 });
102785
- this.internalEnqueueMerge(taskId);
103900
+ if (!this.internalEnqueueMerge(taskId)) {
103901
+ this.manualMergeResolvers.delete(taskId);
103902
+ reject2(new Error(`Merge enqueue rejected for ${taskId}`));
103903
+ }
102786
103904
  });
102787
103905
  }
102788
103906
  setRestoreDiagnostics(outcome, reason, provider, message) {
@@ -102960,7 +104078,7 @@ ${detail}`
102960
104078
  async checkExecutableAvailable(command) {
102961
104079
  const checker = process.platform === "win32" ? "where" : "which";
102962
104080
  try {
102963
- await execFileAsync3(checker, [command]);
104081
+ await execFileAsync4(checker, [command]);
102964
104082
  return { available: true };
102965
104083
  } catch {
102966
104084
  return {
@@ -103049,15 +104167,17 @@ ${detail}`
103049
104167
  return void 0;
103050
104168
  }
103051
104169
  internalEnqueueMerge(taskId) {
103052
- if (this.shuttingDown) return;
104170
+ if (this.shuttingDown) return false;
103053
104171
  if (this.mergeActive.has(taskId)) {
103054
104172
  const isActuallyLive = this.mergeQueue.includes(taskId) || this.activeMergeTaskId === taskId;
103055
104173
  if (!isActuallyLive) {
103056
104174
  runtimeLog.warn(
103057
- `internalEnqueueMerge(${taskId}): skipped \u2014 mergeActive entry is leaked (not queued, not active). reconcileStaleMergeActive() will clear it on the next sweep.`
104175
+ `internalEnqueueMerge(${taskId}): skipped \u2014 mergeActive entry is leaked (not queued, not active). Reconciling stale entry and retrying enqueue now.`
103058
104176
  );
104177
+ this.mergeActive.delete(taskId);
104178
+ } else {
104179
+ return false;
103059
104180
  }
103060
- return;
103061
104181
  }
103062
104182
  this.mergeActive.add(taskId);
103063
104183
  this.mergeQueue.push(taskId);
@@ -103066,6 +104186,7 @@ ${detail}`
103066
104186
  `Merge queue drain failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
103067
104187
  );
103068
104188
  });
104189
+ return true;
103069
104190
  }
103070
104191
  /**
103071
104192
  * Filter a sweep's listTasks() result to merge-eligible tasks, sort by
@@ -103084,6 +104205,27 @@ ${detail}`
103084
104205
  }
103085
104206
  return eligible.length;
103086
104207
  }
104208
+ reconcileStaleMergeActive() {
104209
+ let cleared = 0;
104210
+ for (const taskId of [...this.mergeActive]) {
104211
+ if (taskId === this.activeMergeTaskId) continue;
104212
+ if (this.mergeQueue.includes(taskId)) continue;
104213
+ this.mergeActive.delete(taskId);
104214
+ cleared++;
104215
+ }
104216
+ return cleared;
104217
+ }
104218
+ scheduleMergeActiveReconciliation(intervalMs) {
104219
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
104220
+ return;
104221
+ }
104222
+ this.mergeActiveReconcileTimer = setInterval(() => {
104223
+ const cleared = this.reconcileStaleMergeActive();
104224
+ if (cleared > 0) {
104225
+ runtimeLog.warn(`Reconciled ${cleared} stale mergeActive entr${cleared === 1 ? "y" : "ies"}`);
104226
+ }
104227
+ }, intervalMs);
104228
+ }
103087
104229
  async findActiveRecoveryFollowUp(store, parentTaskId, branch) {
103088
104230
  const tasks = await store.listTasks({ slim: true }).catch(() => []);
103089
104231
  const activeRecoveryTasks = tasks.filter(
@@ -103103,6 +104245,7 @@ ${detail}`
103103
104245
  if (this.mergeRunning) return;
103104
104246
  this.mergeRunning = true;
103105
104247
  try {
104248
+ this.reconcileStaleMergeActive();
103106
104249
  const store = this.runtime.getTaskStore();
103107
104250
  const cwd = this.config.workingDirectory;
103108
104251
  while (this.mergeQueue.length > 0 && !this.shuttingDown) {
@@ -103882,6 +105025,21 @@ ${detail}`
103882
105025
  };
103883
105026
  store.on("settings:updated", onEngineUnpause);
103884
105027
  this.settingsHandlers.push(onEngineUnpause);
105028
+ const onMaintenanceIntervalChange = ({
105029
+ settings: s,
105030
+ previous: prev
105031
+ }) => {
105032
+ if (s.maintenanceIntervalMs === prev.maintenanceIntervalMs) {
105033
+ return;
105034
+ }
105035
+ if (this.mergeActiveReconcileTimer) {
105036
+ clearInterval(this.mergeActiveReconcileTimer);
105037
+ this.mergeActiveReconcileTimer = null;
105038
+ }
105039
+ this.scheduleMergeActiveReconciliation(s.maintenanceIntervalMs ?? 9e5);
105040
+ };
105041
+ store.on("settings:updated", onMaintenanceIntervalChange);
105042
+ this.settingsHandlers.push(onMaintenanceIntervalChange);
103885
105043
  const onStuckTimeoutChange = async ({
103886
105044
  settings: s,
103887
105045
  previous: prev
@@ -104981,6 +106139,7 @@ __export(src_exports2, {
104981
106139
  applyAutostashBySha: () => applyAutostashBySha,
104982
106140
  applyUnavailableNodePolicy: () => applyUnavailableNodePolicy,
104983
106141
  assertSafeUrl: () => assertSafeUrl,
106142
+ auditSquashMerge: () => auditSquashMerge,
104984
106143
  buildAgentChatPrompt: () => buildAgentChatPrompt,
104985
106144
  buildNtfyClickUrl: () => buildNtfyClickUrl,
104986
106145
  buildRuntimeResolutionContext: () => buildRuntimeResolutionContext,
@@ -105006,6 +106165,7 @@ __export(src_exports2, {
105006
106165
  extractRuntimeHint: () => extractRuntimeHint,
105007
106166
  extractRuntimeModel: () => extractRuntimeModel,
105008
106167
  fetchWebContent: () => fetchWebContent,
106168
+ formatSquashAuditReport: () => formatSquashAuditReport,
105009
106169
  formatTaskIdentifier: () => formatTaskIdentifier,
105010
106170
  generateReservedWorktreeName: () => generateReservedWorktreeName,
105011
106171
  generateWorktreeName: () => generateWorktreeName,
@@ -105060,6 +106220,7 @@ var init_src2 = __esm({
105060
106220
  init_mission_autopilot();
105061
106221
  init_mission_execution_loop();
105062
106222
  init_merger();
106223
+ init_merger_squash_audit();
105063
106224
  init_reviewer();
105064
106225
  init_pi();
105065
106226
  init_pi();
@@ -105153,6 +106314,7 @@ __export(planning_exports, {
105153
106314
  getRateLimitResetTime: () => getRateLimitResetTime2,
105154
106315
  getSession: () => getSession,
105155
106316
  getSummary: () => getSummary,
106317
+ mergePlanningSubtaskDrafts: () => mergePlanningSubtaskDrafts,
105156
106318
  parseAgentResponse: () => parseAgentResponse,
105157
106319
  planningStreamManager: () => planningStreamManager,
105158
106320
  rehydrateFromStore: () => rehydrateFromStore,
@@ -106505,6 +107667,37 @@ function generateSubtasksFromPlanning(sessionId) {
106505
107667
  }
106506
107668
  ];
106507
107669
  }
107670
+ function mergePlanningSubtaskDrafts(sessionId, drafts) {
107671
+ const generatedSubtasks = generateSubtasksFromPlanning(sessionId);
107672
+ const generatedById = new Map(generatedSubtasks.map((subtask) => [subtask.id, subtask]));
107673
+ return drafts.map((draft) => {
107674
+ const generated = generatedById.get(draft.id);
107675
+ const normalizedDependsOn = Array.isArray(draft.dependsOn) ? draft.dependsOn.filter((dependency) => typeof dependency === "string") : void 0;
107676
+ if (!generated) {
107677
+ const title = typeof draft.title === "string" ? draft.title.trim() : "";
107678
+ if (!title) {
107679
+ throw new Error(`Client-added subtask must have a title: ${draft.id}`);
107680
+ }
107681
+ const description = typeof draft.description === "string" ? draft.description : title;
107682
+ return {
107683
+ id: draft.id,
107684
+ title,
107685
+ description,
107686
+ suggestedSize: draft.suggestedSize === "S" || draft.suggestedSize === "M" || draft.suggestedSize === "L" ? draft.suggestedSize : "M",
107687
+ priority: draft.priority ?? DEFAULT_TASK_PRIORITY,
107688
+ dependsOn: normalizedDependsOn ?? []
107689
+ };
107690
+ }
107691
+ return {
107692
+ id: generated.id,
107693
+ title: typeof draft.title === "string" ? draft.title : generated.title,
107694
+ description: typeof draft.description === "string" ? draft.description : generated.description,
107695
+ suggestedSize: draft.suggestedSize === "S" || draft.suggestedSize === "M" || draft.suggestedSize === "L" ? draft.suggestedSize : generated.suggestedSize,
107696
+ priority: draft.priority ?? generated.priority ?? DEFAULT_TASK_PRIORITY,
107697
+ dependsOn: normalizedDependsOn ?? generated.dependsOn
107698
+ };
107699
+ });
107700
+ }
106508
107701
  function cleanupSession(sessionId) {
106509
107702
  cleanupInMemorySession(sessionId);
106510
107703
  unpersistSession(sessionId);
@@ -111876,10 +113069,10 @@ var init_github_poll = __esm({
111876
113069
  });
111877
113070
 
111878
113071
  // ../dashboard/src/routes/resolve-diff-base.ts
111879
- import { execFile as execFile6 } from "node:child_process";
111880
- import { promisify as promisify12 } from "node:util";
113072
+ import { execFile as execFile7 } from "node:child_process";
113073
+ import { promisify as promisify13 } from "node:util";
111881
113074
  async function runGitCommand(args, cwd, timeout2 = 1e4) {
111882
- const result = await execFileAsync4("git", args, {
113075
+ const result = await execFileAsync5("git", args, {
111883
113076
  cwd,
111884
113077
  timeout: timeout2,
111885
113078
  maxBuffer: 10 * 1024 * 1024,
@@ -111953,11 +113146,11 @@ async function resolveDiffBase(task, cwd, headRef = "HEAD", runGit = runGitComma
111953
113146
  return void 0;
111954
113147
  }
111955
113148
  }
111956
- var execFileAsync4;
113149
+ var execFileAsync5;
111957
113150
  var init_resolve_diff_base = __esm({
111958
113151
  "../dashboard/src/routes/resolve-diff-base.ts"() {
111959
113152
  "use strict";
111960
- execFileAsync4 = promisify12(execFile6);
113153
+ execFileAsync5 = promisify13(execFile7);
111961
113154
  }
111962
113155
  });
111963
113156
 
@@ -117122,7 +118315,7 @@ function registerPlanningSubtaskRoutes(ctx, deps) {
117122
118315
  throw badRequest("subtasks must be a non-empty array");
117123
118316
  }
117124
118317
  const { store: scopedStore } = await getProjectContext3(req);
117125
- const { getSession: getSession2, cleanupSession: cleanupSession2, formatInterviewQA: formatInterviewQA2 } = await Promise.resolve().then(() => (init_planning(), planning_exports));
118318
+ const { getSession: getSession2, cleanupSession: cleanupSession2, formatInterviewQA: formatInterviewQA2, mergePlanningSubtaskDrafts: mergePlanningSubtaskDrafts2 } = await Promise.resolve().then(() => (init_planning(), planning_exports));
117126
118319
  const session = getSession2(planningSessionId);
117127
118320
  if (!session) {
117128
118321
  throw notFound(`Planning session ${planningSessionId} not found or expired`);
@@ -117135,12 +118328,33 @@ function registerPlanningSubtaskRoutes(ctx, deps) {
117135
118328
 
117136
118329
  ${qaSection}` : `Source: ${session.initialPlan.slice(0, 200)}`;
117137
118330
  for (const item of subtasks) {
117138
- if (!item || typeof item.id !== "string" || typeof item.title !== "string" || !item.title.trim()) {
117139
- throw badRequest("Each subtask must include id and title");
118331
+ if (!item || typeof item.id !== "string" || !item.id.trim()) {
118332
+ throw badRequest("Each subtask must include id");
118333
+ }
118334
+ if (item.title !== void 0 && (typeof item.title !== "string" || !item.title.trim())) {
118335
+ throw badRequest("Each edited subtask title must be a non-empty string");
118336
+ }
118337
+ if (item.description !== void 0 && typeof item.description !== "string") {
118338
+ throw badRequest("Each edited subtask description must be a string");
118339
+ }
118340
+ if (item.suggestedSize !== void 0 && item.suggestedSize !== "S" && item.suggestedSize !== "M" && item.suggestedSize !== "L") {
118341
+ throw badRequest("Each edited subtask suggestedSize must be S, M, or L");
117140
118342
  }
117141
118343
  if (item.priority !== void 0 && !isTaskPriority2(item.priority)) {
117142
118344
  throw badRequest("Each subtask priority must be one of low, normal, high, urgent");
117143
118345
  }
118346
+ if (item.dependsOn !== void 0 && (!Array.isArray(item.dependsOn) || item.dependsOn.some((dependency) => typeof dependency !== "string"))) {
118347
+ throw badRequest("Each edited subtask dependsOn value must be an array of ids");
118348
+ }
118349
+ }
118350
+ let mergedSubtasks;
118351
+ try {
118352
+ mergedSubtasks = mergePlanningSubtaskDrafts2(planningSessionId, subtasks);
118353
+ } catch (error) {
118354
+ throw badRequest(error instanceof Error ? error.message : "Invalid planning subtask edits");
118355
+ }
118356
+ if (mergedSubtasks.length !== subtasks.length) {
118357
+ throw badRequest("Could not resolve planning subtasks for task creation");
117144
118358
  }
117145
118359
  const { branch: resolvedBranch, baseBranch: resolvedBaseBranch } = resolveBranchSelection(branchSelection, branch, baseBranch);
117146
118360
  const { mode: branchMode } = resolveBranchAssignmentContext(branchAssignment);
@@ -117152,7 +118366,7 @@ ${qaSection}` : `Source: ${session.initialPlan.slice(0, 200)}`;
117152
118366
  };
117153
118367
  const createdTasks = [];
117154
118368
  const tempIdToTaskId = /* @__PURE__ */ new Map();
117155
- for (const item of subtasks) {
118369
+ for (const item of mergedSubtasks) {
117156
118370
  const taskBranch = branchMode === "per-task-derived" ? derivePerTaskBranch(resolvedBranch, item.title || item.id) : resolvedBranch;
117157
118371
  const task = await scopedStore.createTask({
117158
118372
  title: item.title.trim(),
@@ -117172,8 +118386,8 @@ ${qaSection}` : `Source: ${session.initialPlan.slice(0, 200)}`;
117172
118386
  await scopedStore.updateTask(task.id, { size: item.suggestedSize });
117173
118387
  }
117174
118388
  }
117175
- for (let index2 = 0; index2 < subtasks.length; index2++) {
117176
- const item = subtasks[index2];
118389
+ for (let index2 = 0; index2 < mergedSubtasks.length; index2++) {
118390
+ const item = mergedSubtasks[index2];
117177
118391
  const created = createdTasks[index2];
117178
118392
  const resolvedDependencies = Array.isArray(item.dependsOn) ? item.dependsOn.map((dep) => tempIdToTaskId.get(dep)).filter((dep) => Boolean(dep)) : [];
117179
118393
  if (resolvedDependencies.length > 0) {
@@ -117413,6 +118627,43 @@ function formatAttachmentSize(size) {
117413
118627
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`;
117414
118628
  return `${(size / (1024 * 1024)).toFixed(1)}MB`;
117415
118629
  }
118630
+ function normalizeFailureCode(code) {
118631
+ if (typeof code === "string" && code.trim()) {
118632
+ return code.trim();
118633
+ }
118634
+ if (typeof code === "number" && Number.isFinite(code)) {
118635
+ return String(code);
118636
+ }
118637
+ return void 0;
118638
+ }
118639
+ function buildChatFailureInfo(error, fallbackSummary = "AI processing failed") {
118640
+ if (typeof error === "string") {
118641
+ const summary = error.trim() || fallbackSummary;
118642
+ return { summary };
118643
+ }
118644
+ if (error && typeof error === "object") {
118645
+ const record = error;
118646
+ const summary = typeof record.message === "string" && record.message.trim() ? record.message.trim() : fallbackSummary;
118647
+ const detail = typeof record.stack === "string" && record.stack.trim() && record.stack.trim() !== summary ? record.stack.trim() : void 0;
118648
+ return {
118649
+ summary,
118650
+ ...typeof record.name === "string" && record.name.trim() && record.name.trim() !== "Error" ? { errorClass: record.name.trim() } : {},
118651
+ ...normalizeFailureCode(record.code) ? { code: normalizeFailureCode(record.code) } : {},
118652
+ ...detail ? { detail } : {}
118653
+ };
118654
+ }
118655
+ return { summary: fallbackSummary };
118656
+ }
118657
+ function persistFailureMessage(chatStore, sessionId, failureInfo, metadata) {
118658
+ return chatStore.addMessage(sessionId, {
118659
+ role: "assistant",
118660
+ content: failureInfo.summary,
118661
+ metadata: {
118662
+ failureInfo,
118663
+ ...metadata ?? {}
118664
+ }
118665
+ });
118666
+ }
117416
118667
  function validateFilePath(basePath, filePath) {
117417
118668
  if (filePath.includes("\0")) {
117418
118669
  throw new Error(`Access denied: Invalid characters in path`);
@@ -118074,18 +119325,26 @@ ${CHAT_AGENT_MESSAGE_ROUTING_GUIDANCE}`;
118074
119325
  "Latest user message to answer:",
118075
119326
  input.content
118076
119327
  ].join("\n\n");
119328
+ const responderRuntimeModel = extractRuntimeModel(input.responder.runtimeConfig);
119329
+ const effectiveModelProvider = input.modelProvider ?? responderRuntimeModel.provider;
119330
+ const effectiveModelId = input.modelId ?? responderRuntimeModel.modelId;
119331
+ const chatModelSettings = await this.getChatModelSettings();
119332
+ const allowFallback = !(input.modelProvider && input.modelId) && !(responderRuntimeModel.provider && responderRuntimeModel.modelId);
118077
119333
  const resolvedSession = await createResolvedAgentSession2({
118078
- createFnAgent: createFnAgent8,
118079
- resolvedProvider: input.modelProvider,
118080
- resolvedModel: input.modelId,
118081
- defaultModelProvider: input.modelProvider,
118082
- defaultModelId: input.modelId,
118083
- createFnAgentArgs: {
118084
- rootDir: this.rootDir,
118085
- modelProvider: input.modelProvider,
118086
- modelId: input.modelId,
118087
- systemPrompt
118088
- }
119334
+ sessionPurpose: "heartbeat",
119335
+ pluginRunner: this.pluginRunner,
119336
+ runtimeHint: extractRuntimeHint(input.responder.runtimeConfig),
119337
+ cwd: this.rootDir,
119338
+ systemPrompt,
119339
+ tools: "coding",
119340
+ ...effectiveModelProvider && effectiveModelId ? {
119341
+ defaultProvider: effectiveModelProvider,
119342
+ defaultModelId: effectiveModelId
119343
+ } : {},
119344
+ ...allowFallback && chatModelSettings.fallbackProvider && chatModelSettings.fallbackModelId ? {
119345
+ fallbackProvider: chatModelSettings.fallbackProvider,
119346
+ fallbackModelId: chatModelSettings.fallbackModelId
119347
+ } : {}
118089
119348
  });
118090
119349
  try {
118091
119350
  await promptWithFallback(resolvedSession.session, roomPrompt);
@@ -118400,9 +119659,12 @@ ${mentionContext}`;
118400
119659
  }
118401
119660
  const sessionErrorMessage = agentResult.session.state.errorMessage;
118402
119661
  if (typeof sessionErrorMessage === "string" && sessionErrorMessage.trim().length > 0 && !accumulatedText && !accumulatedThinking && toolCallsAccum.length === 0) {
119662
+ const failureInfo = buildChatFailureInfo(sessionErrorMessage, "Model response failed");
119663
+ persistFailureMessage(this.chatStore, sessionId, failureInfo);
119664
+ this.flushInFlightGenerationPersist(sessionId, null);
118403
119665
  chatStreamManager.broadcast(sessionId, {
118404
119666
  type: "error",
118405
- data: sessionErrorMessage
119667
+ data: failureInfo
118406
119668
  }, broadcastOptions);
118407
119669
  return;
118408
119670
  }
@@ -118456,7 +119718,7 @@ ${mentionContext}`;
118456
119718
  }, broadcastOptions);
118457
119719
  return;
118458
119720
  }
118459
- const errorMessage = err instanceof Error ? err.message : "AI processing failed";
119721
+ const failureInfo = buildChatFailureInfo(err, "AI processing failed");
118460
119722
  diagnostics6.error(`Error in sendMessage for session ${sessionId}:`, err);
118461
119723
  if (accumulatedText || accumulatedThinking || toolCallsAccum.length > 0) {
118462
119724
  try {
@@ -118474,10 +119736,15 @@ ${mentionContext}`;
118474
119736
  diagnostics6.error(`Failed to persist partial response for session ${sessionId}:`, persistErr);
118475
119737
  }
118476
119738
  }
119739
+ try {
119740
+ persistFailureMessage(this.chatStore, sessionId, failureInfo, fallbackInfo ? { fallback: fallbackInfo } : void 0);
119741
+ } catch (persistErr) {
119742
+ diagnostics6.error(`Failed to persist failure message for session ${sessionId}:`, persistErr);
119743
+ }
118477
119744
  this.flushInFlightGenerationPersist(sessionId, null);
118478
119745
  chatStreamManager.broadcast(sessionId, {
118479
119746
  type: "error",
118480
- data: errorMessage
119747
+ data: failureInfo
118481
119748
  }, broadcastOptions);
118482
119749
  } finally {
118483
119750
  const current = this.activeGenerations.get(sessionId);
@@ -124157,16 +125424,16 @@ var init_remote_auth = __esm({
124157
125424
 
124158
125425
  // ../dashboard/src/routes/register-settings-memory-routes.ts
124159
125426
  import crypto from "node:crypto";
124160
- import { execFile as execFile7 } from "node:child_process";
125427
+ import { execFile as execFile8 } from "node:child_process";
124161
125428
  import { homedir as homedir8 } from "node:os";
124162
- import { promisify as promisify13 } from "node:util";
125429
+ import { promisify as promisify14 } from "node:util";
124163
125430
  function registerSettingsMemoryRoutes(ctx, deps) {
124164
125431
  const { router, options, store, runtimeLogger, getProjectContext: getProjectContext3, rethrowAsApiError: rethrowAsApiError8 } = ctx;
124165
125432
  const { githubToken, validateModelPresets: validateModelPresets2, sanitizeOverlapIgnorePaths: sanitizeOverlapIgnorePaths2, discoverDashboardPiExtensions: discoverDashboardPiExtensions2 } = deps;
124166
- const execFileAsync8 = promisify13(execFile7);
125433
+ const execFileAsync9 = promisify14(execFile8);
124167
125434
  async function queryTailscaleFunnelUrl(targetPort) {
124168
125435
  try {
124169
- const { stdout } = await execFileAsync8("tailscale", ["status", "--json"], { timeout: 3e3 });
125436
+ const { stdout } = await execFileAsync9("tailscale", ["status", "--json"], { timeout: 3e3 });
124170
125437
  const data = JSON.parse(stdout);
124171
125438
  const dnsName = data.Self?.DNSName?.replace(/\.$/, "");
124172
125439
  if (!dnsName) return null;
@@ -124179,7 +125446,7 @@ function registerSettingsMemoryRoutes(ctx, deps) {
124179
125446
  async function isCloudflaredAvailable() {
124180
125447
  const command = process.platform === "win32" ? "where" : "which";
124181
125448
  try {
124182
- await execFileAsync8(command, ["cloudflared"]);
125449
+ await execFileAsync9(command, ["cloudflared"]);
124183
125450
  return true;
124184
125451
  } catch {
124185
125452
  return false;
@@ -124234,7 +125501,7 @@ function registerSettingsMemoryRoutes(ctx, deps) {
124234
125501
  if (process.platform === "win32") {
124235
125502
  const command = resolveCloudflaredInstallCommand();
124236
125503
  try {
124237
- await execFileAsync8("winget", ["install", "Cloudflare.cloudflared"], { timeout: 12e4 });
125504
+ await execFileAsync9("winget", ["install", "Cloudflare.cloudflared"], { timeout: 12e4 });
124238
125505
  return { success: true, command };
124239
125506
  } catch (error) {
124240
125507
  return { success: false, command, error: formatExecError(error) };
@@ -124246,21 +125513,21 @@ function registerSettingsMemoryRoutes(ctx, deps) {
124246
125513
  const tempPath = "/tmp/cloudflared";
124247
125514
  const installFromDirectDownload = async () => {
124248
125515
  attemptedCommands.push(`curl -L --output ${tempPath} ${downloadUrl}`);
124249
- await execFileAsync8("curl", ["-L", "--output", tempPath, downloadUrl], { timeout: 12e4 });
125516
+ await execFileAsync9("curl", ["-L", "--output", tempPath, downloadUrl], { timeout: 12e4 });
124250
125517
  attemptedCommands.push(`chmod +x ${tempPath}`);
124251
- await execFileAsync8("chmod", ["+x", tempPath], { timeout: 3e4 });
125518
+ await execFileAsync9("chmod", ["+x", tempPath], { timeout: 3e4 });
124252
125519
  const globalInstallPath = "/usr/local/bin/cloudflared";
124253
125520
  attemptedCommands.push(`mv ${tempPath} ${globalInstallPath}`);
124254
125521
  try {
124255
- await execFileAsync8("mv", [tempPath, globalInstallPath], { timeout: 3e4 });
125522
+ await execFileAsync9("mv", [tempPath, globalInstallPath], { timeout: 3e4 });
124256
125523
  } catch (error) {
124257
125524
  const localBinDir = `${homedir8()}/.local/bin`;
124258
125525
  const localInstallPath = `${localBinDir}/cloudflared`;
124259
125526
  attemptedCommands.push(`mkdir -p ${localBinDir}`);
124260
125527
  attemptedCommands.push(`mv ${tempPath} ${localInstallPath}`);
124261
- await execFileAsync8("mkdir", ["-p", localBinDir], { timeout: 3e4 });
125528
+ await execFileAsync9("mkdir", ["-p", localBinDir], { timeout: 3e4 });
124262
125529
  try {
124263
- await execFileAsync8("mv", [tempPath, localInstallPath], { timeout: 3e4 });
125530
+ await execFileAsync9("mv", [tempPath, localInstallPath], { timeout: 3e4 });
124264
125531
  } catch (fallbackError) {
124265
125532
  throw new Error(
124266
125533
  `Failed to install cloudflared to /usr/local/bin and ~/.local/bin (${formatExecError(error)}; fallback: ${formatExecError(fallbackError)})`
@@ -124271,9 +125538,9 @@ function registerSettingsMemoryRoutes(ctx, deps) {
124271
125538
  if (process.platform === "darwin") {
124272
125539
  attemptedCommands.push("which brew");
124273
125540
  try {
124274
- await execFileAsync8("which", ["brew"], { timeout: 15e3 });
125541
+ await execFileAsync9("which", ["brew"], { timeout: 15e3 });
124275
125542
  attemptedCommands.push("brew install cloudflared");
124276
- await execFileAsync8("brew", ["install", "cloudflared"], { timeout: 12e4 });
125543
+ await execFileAsync9("brew", ["install", "cloudflared"], { timeout: 12e4 });
124277
125544
  return { success: true, command: attemptedCommands.join(" && ") };
124278
125545
  } catch {
124279
125546
  try {
@@ -125490,15 +126757,26 @@ function registerSettingsMemoryRoutes(ctx, deps) {
125490
126757
  }
125491
126758
  const requestOverride = typeof overrideValue === "string" && overrideValue.trim() ? normalizeNtfyBaseUrl(overrideValue, "request") : void 0;
125492
126759
  const storedServer = typeof settings.ntfyBaseUrl === "string" && settings.ntfyBaseUrl.trim() ? normalizeNtfyBaseUrl(settings.ntfyBaseUrl, "settings") : void 0;
126760
+ const tokenOverride = req.body?.ntfyAccessToken;
126761
+ if (tokenOverride !== void 0 && tokenOverride !== null && typeof tokenOverride !== "string") {
126762
+ throw badRequest("ntfy access token must be a string");
126763
+ }
126764
+ const requestToken = typeof tokenOverride === "string" && tokenOverride.trim() ? tokenOverride.trim() : void 0;
126765
+ const storedToken = typeof settings.ntfyAccessToken === "string" && settings.ntfyAccessToken.trim() ? settings.ntfyAccessToken.trim() : void 0;
125493
126766
  const ntfyBaseUrl = requestOverride ?? storedServer ?? "https://ntfy.sh";
125494
126767
  const url = `${ntfyBaseUrl}/${topic}`;
126768
+ const headers = {
126769
+ "Title": "Fusion test notification",
126770
+ "Priority": "default",
126771
+ "Content-Type": "text/plain"
126772
+ };
126773
+ const ntfyAccessToken = requestToken ?? storedToken;
126774
+ if (ntfyAccessToken) {
126775
+ headers.Authorization = `Bearer ${ntfyAccessToken}`;
126776
+ }
125495
126777
  const response = await fetch(url, {
125496
126778
  method: "POST",
125497
- headers: {
125498
- "Title": "Fusion test notification",
125499
- "Priority": "default",
125500
- "Content-Type": "text/plain"
125501
- },
126779
+ headers,
125502
126780
  body: "Fusion test notification \u2014 your notifications are working!"
125503
126781
  });
125504
126782
  if (!response.ok) {
@@ -125592,15 +126870,26 @@ function registerSettingsMemoryRoutes(ctx, deps) {
125592
126870
  }
125593
126871
  const requestOverride = typeof overrideValue === "string" && overrideValue.trim() ? normalizeNtfyBaseUrl(overrideValue, "request") : void 0;
125594
126872
  const storedServer = typeof settings.ntfyBaseUrl === "string" && settings.ntfyBaseUrl.trim() ? normalizeNtfyBaseUrl(settings.ntfyBaseUrl, "settings") : void 0;
126873
+ const tokenOverride = config.ntfyAccessToken ?? body.ntfyAccessToken;
126874
+ if (tokenOverride !== void 0 && tokenOverride !== null && typeof tokenOverride !== "string") {
126875
+ throw badRequest("ntfy access token must be a string");
126876
+ }
126877
+ const requestToken = typeof tokenOverride === "string" && tokenOverride.trim() ? tokenOverride.trim() : void 0;
126878
+ const storedToken = typeof settings.ntfyAccessToken === "string" && settings.ntfyAccessToken.trim() ? settings.ntfyAccessToken.trim() : void 0;
125595
126879
  const ntfyBaseUrl = requestOverride ?? storedServer ?? "https://ntfy.sh";
125596
126880
  const url = `${ntfyBaseUrl}/${topic}`;
126881
+ const headers = {
126882
+ "Title": "Fusion test notification",
126883
+ "Priority": "default",
126884
+ "Content-Type": "text/plain"
126885
+ };
126886
+ const ntfyAccessToken = requestToken ?? storedToken;
126887
+ if (ntfyAccessToken) {
126888
+ headers.Authorization = `Bearer ${ntfyAccessToken}`;
126889
+ }
125597
126890
  const response = await fetch(url, {
125598
126891
  method: "POST",
125599
- headers: {
125600
- "Title": "Fusion test notification",
125601
- "Priority": "default",
125602
- "Content-Type": "text/plain"
125603
- },
126892
+ headers,
125604
126893
  body: "Fusion test notification \u2014 your notifications are working!"
125605
126894
  });
125606
126895
  if (!response.ok) {
@@ -154429,13 +155718,13 @@ var init_register_agents_projects_nodes = __esm({
154429
155718
  });
154430
155719
 
154431
155720
  // ../dashboard/src/exec-file.ts
154432
- import { execFile as execFile8 } from "node:child_process";
154433
- import { promisify as promisify14 } from "node:util";
154434
- var execFileAsync5;
155721
+ import { execFile as execFile9 } from "node:child_process";
155722
+ import { promisify as promisify15 } from "node:util";
155723
+ var execFileAsync6;
154435
155724
  var init_exec_file = __esm({
154436
155725
  "../dashboard/src/exec-file.ts"() {
154437
155726
  "use strict";
154438
- execFileAsync5 = promisify14(execFile8);
155727
+ execFileAsync6 = promisify15(execFile9);
154439
155728
  }
154440
155729
  });
154441
155730
 
@@ -154646,7 +155935,7 @@ var init_register_project_routes = __esm({
154646
155935
  throw badRequest("cloneUrl must be a non-empty string when provided");
154647
155936
  }
154648
155937
  try {
154649
- await execFileAsync5("git", ["clone", cloneSource, normalizedPath], {
155938
+ await execFileAsync6("git", ["clone", cloneSource, normalizedPath], {
154650
155939
  timeout: 9e4,
154651
155940
  maxBuffer: 10 * 1024 * 1024,
154652
155941
  encoding: "utf-8"
@@ -156358,8 +157647,8 @@ var init_register_settings_sync_routes = __esm({
156358
157647
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
156359
157648
  version: 1
156360
157649
  };
156361
- const { createHash: createHash8 } = await import("node:crypto");
156362
- const checksum = createHash8("sha256").update(JSON.stringify(payload)).digest("hex");
157650
+ const { createHash: createHash9 } = await import("node:crypto");
157651
+ const checksum = createHash9("sha256").update(JSON.stringify(payload)).digest("hex");
156363
157652
  await fetchFromRemoteNode(node, "/api/settings/sync-receive", {
156364
157653
  method: "POST",
156365
157654
  body: { ...payload, checksum }
@@ -156418,7 +157707,7 @@ var init_register_settings_sync_routes = __esm({
156418
157707
  });
156419
157708
  return;
156420
157709
  }
156421
- const { createHash: createHash8 } = await import("node:crypto");
157710
+ const { createHash: createHash9 } = await import("node:crypto");
156422
157711
  const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
156423
157712
  const payloadWithoutChecksum = {
156424
157713
  global: remoteSettings.global,
@@ -156426,7 +157715,7 @@ var init_register_settings_sync_routes = __esm({
156426
157715
  exportedAt,
156427
157716
  version: 1
156428
157717
  };
156429
- const checksum = createHash8("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
157718
+ const checksum = createHash9("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
156430
157719
  const result = await central.applyRemoteSettings({
156431
157720
  ...payloadWithoutChecksum,
156432
157721
  checksum
@@ -166247,7 +167536,7 @@ import * as child_process from "node:child_process";
166247
167536
  function getHomeDir6() {
166248
167537
  return process.env.HOME || process.env.USERPROFILE || os4.homedir();
166249
167538
  }
166250
- function execFileAsync6(file, args, options) {
167539
+ function execFileAsync7(file, args, options) {
166251
167540
  return new Promise((resolve52, reject2) => {
166252
167541
  child_process.execFile(file, args, options, (error, stdout, stderr) => {
166253
167542
  if (error) {
@@ -166396,7 +167685,7 @@ async function readConfiguredApiKey(provider, authStorage) {
166396
167685
  }
166397
167686
  async function readClaudeKeychainCredentials() {
166398
167687
  try {
166399
- const { stdout } = await execFileAsync6(
167688
+ const { stdout } = await execFileAsync7(
166400
167689
  "security",
166401
167690
  ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
166402
167691
  { encoding: "utf-8", timeout: 5e3 }
@@ -167289,13 +168578,13 @@ async function fetchGitHubCopilotUsage() {
167289
168578
  windows: []
167290
168579
  };
167291
168580
  try {
167292
- await execFileAsync6("gh", ["auth", "status"], { encoding: "utf-8", timeout: 5e3 });
168581
+ await execFileAsync7("gh", ["auth", "status"], { encoding: "utf-8", timeout: 5e3 });
167293
168582
  } catch {
167294
168583
  usage.error = "GitHub CLI not authenticated \u2014 run 'gh auth login'";
167295
168584
  return usage;
167296
168585
  }
167297
168586
  try {
167298
- const { stdout } = await execFileAsync6("gh", ["api", "/user/copilot", "--jq", "."], {
168587
+ const { stdout } = await execFileAsync7("gh", ["api", "/user/copilot", "--jq", "."], {
167299
168588
  encoding: "utf-8",
167300
168589
  timeout: 1e4
167301
168590
  });
@@ -179180,9 +180469,9 @@ function createApiRoutes(store, options) {
179180
180469
  return Math.max(0, Number((usedMicros / elapsedMicros * 100).toFixed(1)));
179181
180470
  };
179182
180471
  const getVitestProcessIds = async () => {
179183
- const { execFile: execFile11 } = await import("node:child_process");
180472
+ const { execFile: execFile12 } = await import("node:child_process");
179184
180473
  const stdout = await new Promise((resolve52) => {
179185
- execFile11("pgrep", ["-f", "vitest"], { encoding: "utf8" }, (err, out) => {
180474
+ execFile12("pgrep", ["-f", "vitest"], { encoding: "utf8" }, (err, out) => {
179186
180475
  resolve52(err ? "" : typeof out === "string" ? out : "");
179187
180476
  });
179188
180477
  });
@@ -181499,8 +182788,8 @@ function truncateAutomationOutput(stdout, stderr) {
181499
182788
  }
181500
182789
  async function executeSingleCommand(command, timeoutMs, startedAt) {
181501
182790
  const { exec: exec15 } = await import("node:child_process");
181502
- const { promisify: promisify20 } = await import("node:util");
181503
- const execAsyncFn = promisify20(exec15);
182791
+ const { promisify: promisify21 } = await import("node:util");
182792
+ const execAsyncFn = promisify21(exec15);
181504
182793
  const isWindows = process.platform === "win32";
181505
182794
  try {
181506
182795
  const { stdout, stderr } = await execAsyncFn(command, {
@@ -184062,7 +185351,7 @@ var require_websocket = __commonJS({
184062
185351
  var http = __require("http");
184063
185352
  var net = __require("net");
184064
185353
  var tls = __require("tls");
184065
- var { randomBytes: randomBytes4, createHash: createHash8 } = __require("crypto");
185354
+ var { randomBytes: randomBytes4, createHash: createHash9 } = __require("crypto");
184066
185355
  var { Duplex, Readable: Readable2 } = __require("stream");
184067
185356
  var { URL: URL2 } = __require("url");
184068
185357
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -184722,7 +186011,7 @@ var require_websocket = __commonJS({
184722
186011
  abortHandshake(websocket, socket, "Invalid Upgrade header");
184723
186012
  return;
184724
186013
  }
184725
- const digest = createHash8("sha1").update(key + GUID).digest("base64");
186014
+ const digest = createHash9("sha1").update(key + GUID).digest("base64");
184726
186015
  if (res.headers["sec-websocket-accept"] !== digest) {
184727
186016
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
184728
186017
  return;
@@ -185089,7 +186378,7 @@ var require_websocket_server = __commonJS({
185089
186378
  var EventEmitter38 = __require("events");
185090
186379
  var http = __require("http");
185091
186380
  var { Duplex } = __require("stream");
185092
- var { createHash: createHash8 } = __require("crypto");
186381
+ var { createHash: createHash9 } = __require("crypto");
185093
186382
  var extension2 = require_extension();
185094
186383
  var PerMessageDeflate2 = require_permessage_deflate();
185095
186384
  var subprotocol2 = require_subprotocol();
@@ -185390,7 +186679,7 @@ var require_websocket_server = __commonJS({
185390
186679
  );
185391
186680
  }
185392
186681
  if (this._state > RUNNING) return abortHandshake(socket, 503);
185393
- const digest = createHash8("sha1").update(key + GUID).digest("base64");
186682
+ const digest = createHash9("sha1").update(key + GUID).digest("base64");
185394
186683
  const headers = [
185395
186684
  "HTTP/1.1 101 Switching Protocols",
185396
186685
  "Upgrade: websocket",
@@ -186679,7 +187968,7 @@ data: ${JSON.stringify({ type: event.type, data: event.data })}
186679
187968
  app.get("/api/health", (_req, res) => {
186680
187969
  const database = store.getDatabaseHealth();
186681
187970
  res.json({
186682
- status: database.corruptionDetected ? "degraded" : "ok",
187971
+ status: database.healthy ? "ok" : "degraded",
186683
187972
  version: cliPackageVersion,
186684
187973
  uptime: Math.floor(process.uptime()),
186685
187974
  database
@@ -187791,7 +189080,7 @@ var init_src6 = __esm({
187791
189080
 
187792
189081
  // src/commands/task-lifecycle.ts
187793
189082
  import { exec as exec11 } from "node:child_process";
187794
- import { promisify as promisify15 } from "node:util";
189083
+ import { promisify as promisify16 } from "node:util";
187795
189084
  function getMergeStrategy(settings) {
187796
189085
  return settings.mergeStrategy ?? "direct";
187797
189086
  }
@@ -187995,7 +189284,7 @@ var init_task_lifecycle = __esm({
187995
189284
  "src/commands/task-lifecycle.ts"() {
187996
189285
  "use strict";
187997
189286
  init_src();
187998
- execAsync9 = promisify15(exec11);
189287
+ execAsync9 = promisify16(exec11);
187999
189288
  }
188000
189289
  });
188001
189290
 
@@ -193113,7 +194402,7 @@ var init_app = __esm({
193113
194402
  // src/commands/dashboard-tui/controller.ts
193114
194403
  import os6 from "node:os";
193115
194404
  import v82 from "node:v8";
193116
- import { execFile as execFile10 } from "node:child_process";
194405
+ import { execFile as execFile11 } from "node:child_process";
193117
194406
  import { appendFileSync as appendFileSync2 } from "node:fs";
193118
194407
  function getAvailableMemory() {
193119
194408
  const fn = os6.availableMemory;
@@ -193377,7 +194666,7 @@ var init_controller = __esm({
193377
194666
  }
193378
194667
  const selfPid = process.pid;
193379
194668
  const stdout = await new Promise((resolve52) => {
193380
- execFile10("pgrep", ["-f", "vitest"], { encoding: "utf8" }, (err, out) => {
194669
+ execFile11("pgrep", ["-f", "vitest"], { encoding: "utf8" }, (err, out) => {
193381
194670
  resolve52(err ? "" : typeof out === "string" ? out : "");
193382
194671
  });
193383
194672
  });
@@ -193963,7 +195252,7 @@ __export(dashboard_exports, {
193963
195252
  });
193964
195253
  import { join as join66, resolve as pathResolve } from "node:path";
193965
195254
  import { execFile as execFileCb } from "node:child_process";
193966
- import { promisify as promisify16 } from "node:util";
195255
+ import { promisify as promisify17 } from "node:util";
193967
195256
  import { stat as stat12, readdir as readdir13, readFile as fsReadFile3 } from "node:fs/promises";
193968
195257
  import { AuthStorage as AuthStorage2, DefaultPackageManager as DefaultPackageManager2, ModelRegistry as ModelRegistry3, discoverAndLoadExtensions as discoverAndLoadExtensions2, createExtensionRuntime as createExtensionRuntime2 } from "@mariozechner/pi-coding-agent";
193969
195258
  function formatRuntimeContext(context) {
@@ -194117,7 +195406,7 @@ function setDiagnosticStoreListenerCheck(check) {
194117
195406
  diagnosticStoreListenerCheck = check;
194118
195407
  }
194119
195408
  async function gitExec(cwd, args) {
194120
- const { stdout } = await execFileAsync7("git", args, { cwd, maxBuffer: 4 * 1024 * 1024 });
195409
+ const { stdout } = await execFileAsync8("git", args, { cwd, maxBuffer: 4 * 1024 * 1024 });
194121
195410
  return stdout;
194122
195411
  }
194123
195412
  async function buildGitStatus(projectPath) {
@@ -195698,7 +196987,7 @@ async function runDashboard(port, opts = {}) {
195698
196987
  listWorktrees: (projectPath) => buildGitWorktrees(projectPath),
195699
196988
  push: async (projectPath) => {
195700
196989
  try {
195701
- const { stdout, stderr } = await execFileAsync7("git", ["push"], { cwd: projectPath, maxBuffer: 4 * 1024 * 1024 });
196990
+ const { stdout, stderr } = await execFileAsync8("git", ["push"], { cwd: projectPath, maxBuffer: 4 * 1024 * 1024 });
195702
196991
  return { success: true, output: (stdout + stderr).trim() };
195703
196992
  } catch (err) {
195704
196993
  const msg = err instanceof Error ? err.stderr ?? err.message : String(err);
@@ -195707,7 +196996,7 @@ async function runDashboard(port, opts = {}) {
195707
196996
  },
195708
196997
  fetch: async (projectPath) => {
195709
196998
  try {
195710
- const { stdout, stderr } = await execFileAsync7("git", ["fetch"], { cwd: projectPath, maxBuffer: 4 * 1024 * 1024 });
196999
+ const { stdout, stderr } = await execFileAsync8("git", ["fetch"], { cwd: projectPath, maxBuffer: 4 * 1024 * 1024 });
195711
197000
  return { success: true, output: (stdout + stderr).trim() };
195712
197001
  } catch (err) {
195713
197002
  const msg = err instanceof Error ? err.stderr ?? err.message : String(err);
@@ -195836,7 +197125,7 @@ async function runDashboard(port, opts = {}) {
195836
197125
  });
195837
197126
  return { dispose };
195838
197127
  }
195839
- var processDiagnosticsRegistered, diagnosticIntervalHandle, DIAGNOSTIC_INTERVAL_MS, diagnosticStartTime, diagnosticDbHealthCheck, diagnosticStoreListenerCheck, STREAM_LOG_FLUSH_IDLE_MS, StreamedLogBuffer, execFileAsync7, FILES_DENYLIST2, FILE_SIZE_LIMIT, BINARY_CHECK_BYTES, MAX_PREVIEW_LINES;
197128
+ var processDiagnosticsRegistered, diagnosticIntervalHandle, DIAGNOSTIC_INTERVAL_MS, diagnosticStartTime, diagnosticDbHealthCheck, diagnosticStoreListenerCheck, STREAM_LOG_FLUSH_IDLE_MS, StreamedLogBuffer, execFileAsync8, FILES_DENYLIST2, FILE_SIZE_LIMIT, BINARY_CHECK_BYTES, MAX_PREVIEW_LINES;
195840
197129
  var init_dashboard = __esm({
195841
197130
  "src/commands/dashboard.ts"() {
195842
197131
  "use strict";
@@ -195919,7 +197208,7 @@ var init_dashboard = __esm({
195919
197208
  }
195920
197209
  }
195921
197210
  };
195922
- execFileAsync7 = promisify16(execFileCb);
197211
+ execFileAsync8 = promisify17(execFileCb);
195923
197212
  FILES_DENYLIST2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "target", "build"]);
195924
197213
  FILE_SIZE_LIMIT = 1024 * 1024;
195925
197214
  BINARY_CHECK_BYTES = 8 * 1024;
@@ -197789,7 +199078,7 @@ __export(task_exports, {
197789
199078
  runTaskUpdate: () => runTaskUpdate
197790
199079
  });
197791
199080
  import { createInterface as createInterface3 } from "node:readline/promises";
197792
- import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync48, readFileSync as readFileSync24 } from "node:fs";
199081
+ import { watchFile, unwatchFile, statSync as statSync7, existsSync as existsSync48, readFileSync as readFileSync24 } from "node:fs";
197793
199082
  import { basename as basename17, join as join70 } from "node:path";
197794
199083
  function getGitHubIssueUrl(sourceMetadata) {
197795
199084
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
@@ -198111,7 +199400,7 @@ async function runTaskLogs(id, options = {}, projectName) {
198111
199400
  let lastPosition = 0;
198112
199401
  let lastSize = 0;
198113
199402
  try {
198114
- const stats = statSync6(logPath);
199403
+ const stats = statSync7(logPath);
198115
199404
  lastSize = stats.size;
198116
199405
  lastPosition = lastSize;
198117
199406
  } catch {
@@ -198128,7 +199417,7 @@ async function runTaskLogs(id, options = {}, projectName) {
198128
199417
  watchFile(logPath, { interval: 1e3 }, () => {
198129
199418
  if (isShuttingDown) return;
198130
199419
  try {
198131
- const stats = statSync6(logPath);
199420
+ const stats = statSync7(logPath);
198132
199421
  if (stats.size < lastPosition) {
198133
199422
  lastPosition = 0;
198134
199423
  }
@@ -199552,7 +200841,7 @@ __export(git_exports, {
199552
200841
  runGitStatus: () => runGitStatus
199553
200842
  });
199554
200843
  import { exec as exec12 } from "node:child_process";
199555
- import { promisify as promisify17 } from "node:util";
200844
+ import { promisify as promisify18 } from "node:util";
199556
200845
  import { createInterface as createInterface4 } from "node:readline/promises";
199557
200846
  async function resolveGitCwd(projectName) {
199558
200847
  if (projectName) {
@@ -199826,7 +201115,7 @@ var init_git = __esm({
199826
201115
  "src/commands/git.ts"() {
199827
201116
  "use strict";
199828
201117
  init_project_context();
199829
- execAsync10 = promisify17(exec12);
201118
+ execAsync10 = promisify18(exec12);
199830
201119
  }
199831
201120
  });
199832
201121
 
@@ -200013,7 +201302,7 @@ var init_memory_backup2 = __esm({
200013
201302
  });
200014
201303
 
200015
201304
  // src/project-resolver.ts
200016
- import { existsSync as existsSync50, statSync as statSync7 } from "node:fs";
201305
+ import { existsSync as existsSync50, statSync as statSync8 } from "node:fs";
200017
201306
  import { basename as basename18, dirname as dirname32, resolve as resolve42, normalize as normalize6 } from "node:path";
200018
201307
  import { createInterface as createInterface5 } from "node:readline/promises";
200019
201308
  async function getCentralCore() {
@@ -200613,7 +201902,7 @@ __export(project_exports, {
200613
201902
  runProjectShow: () => runProjectShow
200614
201903
  });
200615
201904
  import { resolve as resolve43, isAbsolute as isAbsolute21, relative as relative16, basename as basename19 } from "node:path";
200616
- import { existsSync as existsSync51, statSync as statSync8 } from "node:fs";
201905
+ import { existsSync as existsSync51, statSync as statSync9 } from "node:fs";
200617
201906
  import { createInterface as createInterface7 } from "node:readline/promises";
200618
201907
  function formatDisplayPath(projectPath) {
200619
201908
  const rel = relative16(process.cwd(), projectPath);
@@ -200748,7 +202037,7 @@ async function runProjectAdd(name, path6, options = {}) {
200748
202037
  rl.close();
200749
202038
  process.exit(1);
200750
202039
  }
200751
- if (!statSync8(absolutePath2).isDirectory()) {
202040
+ if (!statSync9(absolutePath2).isDirectory()) {
200752
202041
  console.error(`
200753
202042
  \u2717 Path is not a directory: ${projectPath}`);
200754
202043
  rl.close();
@@ -200793,7 +202082,7 @@ async function runProjectAdd(name, path6, options = {}) {
200793
202082
  `);
200794
202083
  process.exit(1);
200795
202084
  }
200796
- if (!statSync8(absolutePath).isDirectory()) {
202085
+ if (!statSync9(absolutePath).isDirectory()) {
200797
202086
  console.error(`
200798
202087
  \u2717 Path is not a directory: ${projectPath}
200799
202088
  `);
@@ -201130,7 +202419,7 @@ __export(init_exports, {
201130
202419
  import { existsSync as existsSync53, mkdirSync as mkdirSync11, writeFileSync as writeFileSync5, readFileSync as readFileSync25 } from "node:fs";
201131
202420
  import { join as join73, resolve as resolve45, basename as basename20 } from "node:path";
201132
202421
  import { exec as exec13 } from "node:child_process";
201133
- import { promisify as promisify18 } from "node:util";
202422
+ import { promisify as promisify19 } from "node:util";
201134
202423
  async function runInit(options = {}) {
201135
202424
  const cwd = options.path ? resolve45(options.path) : process.cwd();
201136
202425
  const fusionDir = join73(cwd, ".fusion");
@@ -201345,7 +202634,7 @@ var init_init = __esm({
201345
202634
  init_claude_skills_runner();
201346
202635
  init_git();
201347
202636
  init_skill_installation();
201348
- execAsync11 = promisify18(exec13);
202637
+ execAsync11 = promisify19(exec13);
201349
202638
  }
201350
202639
  });
201351
202640
 
@@ -201432,7 +202721,7 @@ var agent_import_exports = {};
201432
202721
  __export(agent_import_exports, {
201433
202722
  runAgentImport: () => runAgentImport
201434
202723
  });
201435
- import { existsSync as existsSync54, mkdirSync as mkdirSync12, readFileSync as readFileSync26, statSync as statSync9, writeFileSync as writeFileSync6 } from "node:fs";
202724
+ import { existsSync as existsSync54, mkdirSync as mkdirSync12, readFileSync as readFileSync26, statSync as statSync10, writeFileSync as writeFileSync6 } from "node:fs";
201436
202725
  import { resolve as resolve46 } from "node:path";
201437
202726
  function slugifyPathSegment(input) {
201438
202727
  if (!input || typeof input !== "string") {
@@ -201590,7 +202879,7 @@ async function runAgentImport(source, options) {
201590
202879
  let skills = [];
201591
202880
  let isPackageImport = false;
201592
202881
  try {
201593
- const sourceStats = statSync9(sourcePath);
202882
+ const sourceStats = statSync10(sourcePath);
201594
202883
  if (sourceStats.isDirectory()) {
201595
202884
  const pkg = parseCompanyDirectory(sourcePath);
201596
202885
  companyName = pkg.company?.name;
@@ -203007,7 +204296,7 @@ __export(update_exports, {
203007
204296
  });
203008
204297
  import { exec as exec14 } from "node:child_process";
203009
204298
  import { existsSync as existsSync57, readFileSync as readFileSync27 } from "node:fs";
203010
- import { promisify as promisify19 } from "node:util";
204299
+ import { promisify as promisify20 } from "node:util";
203011
204300
  import { dirname as dirname35, resolve as resolve50 } from "node:path";
203012
204301
  import { fileURLToPath as fileURLToPath16 } from "node:url";
203013
204302
  function readOwnCliVersion() {
@@ -203173,7 +204462,7 @@ var init_update = __esm({
203173
204462
  "src/commands/update.ts"() {
203174
204463
  "use strict";
203175
204464
  init_update_cache();
203176
- execAsync12 = promisify19(exec14);
204465
+ execAsync12 = promisify20(exec14);
203177
204466
  REGISTRY_URL2 = "https://registry.npmjs.org/@runfusion%2Ffusion";
203178
204467
  INSTALL_COMMAND = "npm install -g @runfusion/fusion@latest";
203179
204468
  }
@@ -203840,8 +205129,8 @@ async function main() {
203840
205129
  const name = nameIdx !== -1 && nameIdx + 1 < args.length ? args[nameIdx + 1] : void 0;
203841
205130
  const pathIdx = args.indexOf("--path");
203842
205131
  const path6 = pathIdx !== -1 && pathIdx + 1 < args.length ? args[pathIdx + 1] : void 0;
203843
- const git = args.includes("--git");
203844
- await runInit2({ name, path: path6, git });
205132
+ const git2 = args.includes("--git");
205133
+ await runInit2({ name, path: path6, git: git2 });
203845
205134
  break;
203846
205135
  }
203847
205136
  case "dashboard": {