@runfusion/fusion 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/bin.js +438 -235
  2. package/dist/client/assets/{AgentDetailView-Cu4TgvBn.js → AgentDetailView-Dx7T1-gM.js} +3 -3
  3. package/dist/client/assets/AgentPermissionPolicyEditor-Cbd6SG_T.css +1 -0
  4. package/dist/client/assets/AgentPermissionPolicyEditor-Ia7QkynK.js +6 -0
  5. package/dist/client/assets/{AgentsView-CIH4Acck.js → AgentsView-DBbdPWV-.js} +3 -3
  6. package/dist/client/assets/ChatView-DYpoWnCc.js +1 -0
  7. package/dist/client/assets/{DevServerView-Bd8JZ-3C.js → DevServerView-DotHOFZt.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-BOGqZNiq.js → DirectoryPicker-CIj3ZGKv.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-B-ELcw4o.js → DocumentsView-Dt27gDYa.js} +1 -1
  10. package/dist/client/assets/{EvalsView-Ccl9QBYN.js → EvalsView-HfWTc8Uv.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-BM7EnSvJ.js → ExperimentalAgentOnboardingModal-U7fXDhhN.js} +1 -1
  12. package/dist/client/assets/{InsightsView-DLF49bzu.js → InsightsView-C8bsAlMv.js} +1 -1
  13. package/dist/client/assets/{MemoryView-CE1D9euM.js → MemoryView-D-HLtRrr.js} +1 -1
  14. package/dist/client/assets/{NodesView-Zxusnhxu.js → NodesView-w3ms-XX2.js} +1 -1
  15. package/dist/client/assets/{PiExtensionsManager-BlMmxtLZ.js → PiExtensionsManager-DFAZAfNn.js} +2 -2
  16. package/dist/client/assets/{PluginManager-D4eZ0zA4.js → PluginManager-DN1VG7Qm.js} +1 -1
  17. package/dist/client/assets/ReliabilityView-D0Rtd7pX.css +1 -0
  18. package/dist/client/assets/{ResearchView-C14Zh7c9.js → ResearchView-YoxEFtNB.js} +1 -1
  19. package/dist/client/assets/{SettingsModal-BAx4gt2A.css → SettingsModal-1Y8N6MkL.css} +1 -1
  20. package/dist/client/assets/SettingsModal-B86XhYB1.js +31 -0
  21. package/dist/client/assets/{SettingsModal-DRnZ6eBW.js → SettingsModal-BRRPoxku.js} +1 -1
  22. package/dist/client/assets/{SetupWizardModal-BmmRM3g-.js → SetupWizardModal-B4eA3bpz.js} +1 -1
  23. package/dist/client/assets/{SkillsView-DWVtT2-8.js → SkillsView-DZIfZoF5.js} +1 -1
  24. package/dist/client/assets/{StashRecoveryView-CKkaWzzc.js → StashRecoveryView-mKXYsqam.js} +1 -1
  25. package/dist/client/assets/{TodoView-hbD0NOCY.js → TodoView-C6sLLEQu.js} +1 -1
  26. package/dist/client/assets/dashboard-view-DOY8-BIx.js +21 -0
  27. package/dist/client/assets/{folder-open-BTN2_Wuv.js → folder-open-C_fxRMq1.js} +1 -1
  28. package/dist/client/assets/index-DTFPgu6_.js +695 -0
  29. package/dist/client/assets/index-DdvAZIzj.css +1 -0
  30. package/dist/client/assets/{upload-euJEs_N2.js → upload-xjku_93-.js} +1 -1
  31. package/dist/client/assets/{users-B7miEHCZ.js → users-CbALDtDk.js} +1 -1
  32. package/dist/client/index.html +2 -2
  33. package/dist/client/version.json +1 -1
  34. package/dist/droid-cli/package.json +1 -1
  35. package/dist/extension.js +477 -225
  36. package/dist/pi-claude-cli/package.json +1 -1
  37. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  38. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  39. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  40. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  41. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  42. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  43. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  44. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  45. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  46. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  47. package/package.json +1 -1
  48. package/skill/fusion/SKILL.md +1 -1
  49. package/skill/fusion/references/extension-tools.md +11 -0
  50. package/skill/fusion/references/fusion-capabilities.md +1 -0
  51. package/dist/client/assets/AgentPermissionPolicyEditor-Ceg2Dszn.js +0 -6
  52. package/dist/client/assets/AgentPermissionPolicyEditor-CthjqUL_.css +0 -1
  53. package/dist/client/assets/ChatView-DqJoFquZ.js +0 -1
  54. package/dist/client/assets/ReliabilityView-rxA_HgEG.css +0 -1
  55. package/dist/client/assets/SettingsModal-c1QwWno0.js +0 -31
  56. package/dist/client/assets/dashboard-view-VNPB3ca0.js +0 -21
  57. package/dist/client/assets/index-CZFBq256.css +0 -1
  58. package/dist/client/assets/index-DzSLVOnp.js +0 -695
  59. /package/dist/client/assets/{ReliabilityView-KmH60I_5.js → ReliabilityView-DAuqE-zi.js} +0 -0
package/dist/extension.js CHANGED
@@ -864,6 +864,7 @@ Your job: help users transform high-level goals into structured mission plans wi
864
864
  - Features should be specific and actionable
865
865
  - ALWAYS include verification/acceptance criteria at every level:
866
866
  - Milestone: "verification" field \u2014 how to confirm this phase is complete (e.g., "All API endpoints return correct responses, integration tests pass")
867
+ - Milestone: optional "acceptanceCriteria" field for explicit milestone-level completion bars (if omitted, acceptance can be auto-derived from child feature criteria/descriptions)
867
868
  - Slice: "verification" field \u2014 how to confirm this work unit is done (e.g., "Auth flow works end-to-end from signup through login")
868
869
  - Feature: "acceptanceCriteria" field \u2014 how to verify this specific deliverable (e.g., "JWT tokens expire after 1 hour and refresh correctly")
869
870
  - Suggest sensible defaults and push for specificity
@@ -877,7 +878,7 @@ For questions:
877
878
  {"type": "question", "data": {"id": "unique-id", "type": "text|single_select|multi_select|confirm", "question": "The question text", "description": "Helpful context", "options": [{"id": "opt1", "label": "Option 1", "description": "Details"}]}}
878
879
 
879
880
  For completion (when you have enough information):
880
- {"type": "complete", "data": {"missionTitle": "Refined mission title", "missionDescription": "Comprehensive mission description based on the conversation", "milestones": [{"title": "Milestone title", "description": "What this phase achieves", "verification": "How to confirm this milestone is complete", "slices": [{"title": "Slice title", "description": "What this work unit covers", "verification": "How to confirm this slice is done", "features": [{"title": "Feature title", "description": "What to build", "acceptanceCriteria": "How to verify this feature works"}]}]}]}}`
881
+ {"type": "complete", "data": {"missionTitle": "Refined mission title", "missionDescription": "Comprehensive mission description based on the conversation", "milestones": [{"title": "Milestone title", "description": "What this phase achieves", "verification": "How to confirm this milestone is complete", "acceptanceCriteria": "Optional explicit milestone criteria (omit to auto-derive from features)", "slices": [{"title": "Slice title", "description": "What this work unit covers", "verification": "How to confirm this slice is done", "features": [{"title": "Feature title", "description": "What to build", "acceptanceCriteria": "How to verify this feature works"}]}]}]}}`
881
882
  },
882
883
  "ai-refine-system": {
883
884
  key: "ai-refine-system",
@@ -1449,7 +1450,7 @@ function validateMessageMetadata(metadata) {
1449
1450
  throw new Error("metadata.wakeRecipient must be a boolean");
1450
1451
  }
1451
1452
  }
1452
- var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
1453
+ var THINKING_LEVELS, COLUMNS, DEFAULT_COLUMN, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, THEME_MODES, COLOR_THEMES, MERGE_STRATEGY_OVERLAP_BEHAVIORS, POST_MERGE_AUDIT_MODES, MERGE_AUDIT_AUTO_RECOVERY_MODES, AUTO_RECOVERY_MODES, NOTIFICATION_EVENTS, WORKFLOW_STEP_TEMPLATES, DOCUMENT_KEY_RE, REPO_OVERRIDE_RE, CheckoutConflictError, SHARED_STATE_SNAPSHOT_VERSION, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, AGENT_VALID_TRANSITIONS, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES, AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES, AGENT_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, DEFAULT_HEARTBEAT_PROCEDURE_PATH, DASHBOARD_USER_ID, DASHBOARD_USER_ALIASES;
1453
1454
  var init_types = __esm({
1454
1455
  "../core/src/types.ts"() {
1455
1456
  "use strict";
@@ -1883,6 +1884,36 @@ Do NOT spend time on nits when no real issues exist.`
1883
1884
  "network_api",
1884
1885
  "task_agent_mutation"
1885
1886
  ];
1887
+ AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES = {
1888
+ git_write: ["git commit", "git push", "git merge", "git branch -d", "git worktree add", "write", "edit"],
1889
+ file_write_delete: ["write", "edit", "fn_task_attach"],
1890
+ command_execution: ["bash (non-git)", "read", "find", "grep", "ls"],
1891
+ network_api: ["fn_research_run (web/research)", "fn_web_fetch"],
1892
+ task_agent_mutation: [
1893
+ "fn_task_create",
1894
+ "fn_delegate_task",
1895
+ "fn_task_import_github",
1896
+ "fn_task_import_github_issue",
1897
+ "fn_spawn_agent",
1898
+ "fn_update_agent_config",
1899
+ "fn_update_identity"
1900
+ ]
1901
+ };
1902
+ AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES = [
1903
+ "fn_send_message",
1904
+ "fn_post_room_message",
1905
+ "fn_read_messages",
1906
+ "fn_task_log",
1907
+ "fn_task_done",
1908
+ "fn_heartbeat_done",
1909
+ "fn_task_document_write",
1910
+ "fn_task_document_read",
1911
+ "fn_memory_search",
1912
+ "fn_memory_get",
1913
+ "fn_memory_append",
1914
+ "fn_read_evaluations",
1915
+ "fn_reflect_on_performance"
1916
+ ];
1886
1917
  AGENT_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"];
1887
1918
  AGENT_PERMISSION_POLICY_PRESET_IDS = ["unrestricted", "approval-required", "locked-down", "custom"];
1888
1919
  APPROVAL_REQUEST_STATUSES = ["pending", "approved", "denied", "completed"];
@@ -3725,7 +3756,7 @@ var init_db = __esm({
3725
3756
  DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
3726
3757
  DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
3727
3758
  DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
3728
- SCHEMA_VERSION = 79;
3759
+ SCHEMA_VERSION = 81;
3729
3760
  SCHEMA_SQL = `
3730
3761
  -- Tasks table with JSON columns for nested data
3731
3762
  CREATE TABLE IF NOT EXISTS tasks (
@@ -3780,6 +3811,8 @@ CREATE TABLE IF NOT EXISTS tasks (
3780
3811
  createdAt TEXT NOT NULL,
3781
3812
  updatedAt TEXT NOT NULL,
3782
3813
  columnMovedAt TEXT,
3814
+ firstExecutionAt TEXT,
3815
+ cumulativeActiveMs INTEGER,
3783
3816
  executionStartedAt TEXT,
3784
3817
  executionCompletedAt TEXT,
3785
3818
  -- JSON columns for nested arrays/objects
@@ -4247,6 +4280,7 @@ CREATE TABLE IF NOT EXISTS milestones (
4247
4280
  orderIndex INTEGER NOT NULL,
4248
4281
  interviewState TEXT NOT NULL,
4249
4282
  dependencies TEXT DEFAULT '[]',
4283
+ acceptanceCriteria TEXT,
4250
4284
  createdAt TEXT NOT NULL,
4251
4285
  updatedAt TEXT NOT NULL,
4252
4286
  FOREIGN KEY (missionId) REFERENCES missions(id) ON DELETE CASCADE
@@ -6367,6 +6401,25 @@ This means a caller passed a .fusion directory where a project root was expected
6367
6401
  this.addColumnIfMissing("tasks", "overlapBlockedBy", "TEXT");
6368
6402
  });
6369
6403
  }
6404
+ if (version < 80) {
6405
+ this.applyMigration(80, () => {
6406
+ this.addColumnIfMissing("milestones", "acceptanceCriteria", "TEXT");
6407
+ });
6408
+ }
6409
+ if (version < 81) {
6410
+ this.applyMigration(81, () => {
6411
+ this.addColumnIfMissing("tasks", "firstExecutionAt", "TEXT");
6412
+ this.addColumnIfMissing("tasks", "cumulativeActiveMs", "INTEGER");
6413
+ if (this.hasColumn("tasks", "executionStartedAt")) {
6414
+ this.db.prepare(
6415
+ `UPDATE tasks
6416
+ SET firstExecutionAt = executionStartedAt
6417
+ WHERE firstExecutionAt IS NULL
6418
+ AND executionStartedAt IS NOT NULL`
6419
+ ).run();
6420
+ }
6421
+ });
6422
+ }
6370
6423
  }
6371
6424
  /**
6372
6425
  * Run a single migration step inside a transaction and bump the version.
@@ -10609,6 +10662,21 @@ var init_db_migrate = __esm({
10609
10662
 
10610
10663
  // ../core/src/mission-store.ts
10611
10664
  import { EventEmitter as EventEmitter4 } from "node:events";
10665
+ function deriveMilestoneAcceptanceCriteriaFromFeatures(features) {
10666
+ const lines = features.map((feature) => {
10667
+ const acceptance = feature.acceptanceCriteria?.trim();
10668
+ const description = feature.description?.trim();
10669
+ const text = acceptance && acceptance.length > 0 ? acceptance : description && description.length > 0 ? description : void 0;
10670
+ if (!text) {
10671
+ return void 0;
10672
+ }
10673
+ return `- ${feature.title}: ${text}`;
10674
+ }).filter((line) => Boolean(line));
10675
+ if (lines.length === 0) {
10676
+ return void 0;
10677
+ }
10678
+ return lines.join("\n");
10679
+ }
10612
10680
  var DEFAULT_IMPLEMENTATION_RETRY_BUDGET, MissionStore;
10613
10681
  var init_mission_store = __esm({
10614
10682
  "../core/src/mission-store.ts"() {
@@ -10670,6 +10738,7 @@ var init_mission_store = __esm({
10670
10738
  dependencies: fromJson(row.dependencies) || [],
10671
10739
  planningNotes: row.planningNotes || void 0,
10672
10740
  verification: row.verification || void 0,
10741
+ acceptanceCriteria: row.acceptanceCriteria || void 0,
10673
10742
  validationState: row.validationState || "not_started",
10674
10743
  createdAt: row.createdAt,
10675
10744
  updatedAt: row.updatedAt
@@ -11382,13 +11451,14 @@ var init_mission_store = __esm({
11382
11451
  dependencies: input.dependencies || [],
11383
11452
  planningNotes: input.planningNotes,
11384
11453
  verification: input.verification,
11454
+ acceptanceCriteria: input.acceptanceCriteria,
11385
11455
  validationState: "not_started",
11386
11456
  createdAt: now,
11387
11457
  updatedAt: now
11388
11458
  };
11389
11459
  this.db.prepare(`
11390
- INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
11391
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11460
+ INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, acceptanceCriteria, validationState, createdAt, updatedAt)
11461
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11392
11462
  `).run(
11393
11463
  milestone.id,
11394
11464
  milestone.missionId,
@@ -11400,6 +11470,7 @@ var init_mission_store = __esm({
11400
11470
  toJson(milestone.dependencies),
11401
11471
  milestone.planningNotes ?? null,
11402
11472
  milestone.verification ?? null,
11473
+ milestone.acceptanceCriteria ?? null,
11403
11474
  milestone.validationState,
11404
11475
  milestone.createdAt,
11405
11476
  milestone.updatedAt
@@ -11463,6 +11534,7 @@ var init_mission_store = __esm({
11463
11534
  dependencies = ?,
11464
11535
  planningNotes = ?,
11465
11536
  verification = ?,
11537
+ acceptanceCriteria = ?,
11466
11538
  validationState = ?,
11467
11539
  updatedAt = ?
11468
11540
  WHERE id = ?
@@ -11475,6 +11547,7 @@ var init_mission_store = __esm({
11475
11547
  toJson(updated.dependencies),
11476
11548
  updated.planningNotes ?? null,
11477
11549
  updated.verification ?? null,
11550
+ updated.acceptanceCriteria ?? null,
11478
11551
  updated.validationState || "not_started",
11479
11552
  updated.updatedAt,
11480
11553
  updated.id
@@ -11538,6 +11611,21 @@ var init_mission_store = __esm({
11538
11611
  updateMilestoneInterviewState(id, state) {
11539
11612
  return this.updateMilestone(id, { interviewState: state });
11540
11613
  }
11614
+ applyDerivedMilestoneAcceptanceCriteria(milestoneId) {
11615
+ const milestone = this.getMilestone(milestoneId);
11616
+ if (!milestone) {
11617
+ throw new Error(`Milestone ${milestoneId} not found`);
11618
+ }
11619
+ if (milestone.acceptanceCriteria?.trim()) {
11620
+ return milestone;
11621
+ }
11622
+ const features = this.listSlices(milestoneId).flatMap((slice) => this.listFeatures(slice.id));
11623
+ const derivedAcceptanceCriteria = deriveMilestoneAcceptanceCriteriaFromFeatures(features);
11624
+ if (!derivedAcceptanceCriteria) {
11625
+ return milestone;
11626
+ }
11627
+ return this.updateMilestone(milestoneId, { acceptanceCriteria: derivedAcceptanceCriteria });
11628
+ }
11541
11629
  // ── Slice Operations ───────────────────────────────────────────────
11542
11630
  /**
11543
11631
  * Add a slice to a milestone.
@@ -11809,6 +11897,7 @@ var init_mission_store = __esm({
11809
11897
  this.db.bumpLastModified();
11810
11898
  this.emit("feature:created", feature);
11811
11899
  this.recomputeSliceStatus(sliceId);
11900
+ this.applyDerivedMilestoneAcceptanceCriteria(slice.milestoneId);
11812
11901
  return feature;
11813
11902
  }
11814
11903
  /**
@@ -13178,11 +13267,11 @@ ${feature.acceptanceCriteria}`);
13178
13267
  applied++;
13179
13268
  }
13180
13269
  for (const milestone of snapshot.payload.milestones) {
13181
- this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
13182
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13270
+ this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, acceptanceCriteria, validationState, createdAt, updatedAt)
13271
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13183
13272
  ON CONFLICT(id) DO UPDATE SET title=excluded.title, description=excluded.description, status=excluded.status, orderIndex=excluded.orderIndex,
13184
13273
  interviewState=excluded.interviewState, dependencies=excluded.dependencies, planningNotes=excluded.planningNotes, verification=excluded.verification,
13185
- validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
13274
+ acceptanceCriteria=excluded.acceptanceCriteria, validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
13186
13275
  milestone.id,
13187
13276
  milestone.missionId,
13188
13277
  milestone.title,
@@ -13193,6 +13282,7 @@ ${feature.acceptanceCriteria}`);
13193
13282
  toJsonNullable(milestone.dependencies),
13194
13283
  milestone.planningNotes ?? null,
13195
13284
  milestone.verification ?? null,
13285
+ milestone.acceptanceCriteria ?? null,
13196
13286
  milestone.validationState ?? null,
13197
13287
  milestone.createdAt,
13198
13288
  milestone.updatedAt
@@ -37224,6 +37314,8 @@ var init_store = __esm({
37224
37314
  createdAt: row.createdAt,
37225
37315
  updatedAt: row.updatedAt,
37226
37316
  columnMovedAt: row.columnMovedAt || void 0,
37317
+ firstExecutionAt: row.firstExecutionAt || void 0,
37318
+ cumulativeActiveMs: row.cumulativeActiveMs ?? void 0,
37227
37319
  executionStartedAt: row.executionStartedAt || void 0,
37228
37320
  executionCompletedAt: row.executionCompletedAt || void 0,
37229
37321
  dependencies: fromJson(row.dependencies) || [],
@@ -37351,6 +37443,8 @@ var init_store = __esm({
37351
37443
  createdAt: entry.createdAt,
37352
37444
  updatedAt: entry.updatedAt,
37353
37445
  columnMovedAt: entry.columnMovedAt,
37446
+ firstExecutionAt: entry.firstExecutionAt,
37447
+ cumulativeActiveMs: entry.cumulativeActiveMs,
37354
37448
  executionStartedAt: entry.executionStartedAt,
37355
37449
  executionCompletedAt: entry.executionCompletedAt,
37356
37450
  modelPresetId: entry.modelPresetId,
@@ -37458,6 +37552,8 @@ ${recentText}` : void 0
37458
37552
  createdAt: task.createdAt,
37459
37553
  updatedAt: task.updatedAt,
37460
37554
  columnMovedAt: task.columnMovedAt,
37555
+ firstExecutionAt: task.firstExecutionAt,
37556
+ cumulativeActiveMs: task.cumulativeActiveMs,
37461
37557
  executionStartedAt: task.executionStartedAt,
37462
37558
  executionCompletedAt: task.executionCompletedAt,
37463
37559
  archivedAt,
@@ -37575,6 +37671,8 @@ ${recentText}` : void 0
37575
37671
  "createdAt",
37576
37672
  "updatedAt",
37577
37673
  "columnMovedAt",
37674
+ "firstExecutionAt",
37675
+ "cumulativeActiveMs",
37578
37676
  "executionStartedAt",
37579
37677
  "executionCompletedAt",
37580
37678
  "dependencies",
@@ -37711,6 +37809,8 @@ ${outcome}`;
37711
37809
  "createdAt",
37712
37810
  "updatedAt",
37713
37811
  "columnMovedAt",
37812
+ "firstExecutionAt",
37813
+ "cumulativeActiveMs",
37714
37814
  "executionStartedAt",
37715
37815
  "executionCompletedAt",
37716
37816
  "dependencies",
@@ -37836,6 +37936,8 @@ ${outcome}`;
37836
37936
  task.createdAt,
37837
37937
  task.updatedAt,
37838
37938
  task.columnMovedAt ?? null,
37939
+ task.firstExecutionAt ?? null,
37940
+ task.cumulativeActiveMs ?? null,
37839
37941
  task.executionStartedAt ?? null,
37840
37942
  task.executionCompletedAt ?? null,
37841
37943
  toJson(task.dependencies || []),
@@ -37900,7 +38002,7 @@ ${outcome}`;
37900
38002
  workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount, nextRecoveryAt, error,
37901
38003
  summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
37902
38004
  tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
37903
- executionStartedAt, executionCompletedAt,
38005
+ firstExecutionAt, cumulativeActiveMs, executionStartedAt, executionCompletedAt,
37904
38006
  dependencies, steps, log, attachments, steeringComments,
37905
38007
  comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
37906
38008
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
@@ -37924,7 +38026,7 @@ ${outcome}`;
37924
38026
  workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount, nextRecoveryAt, error,
37925
38027
  summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
37926
38028
  tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
37927
- executionStartedAt, executionCompletedAt,
38029
+ firstExecutionAt, cumulativeActiveMs, executionStartedAt, executionCompletedAt,
37928
38030
  dependencies, steps, log, attachments, steeringComments,
37929
38031
  comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
37930
38032
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
@@ -37986,6 +38088,8 @@ ${outcome}`;
37986
38088
  createdAt = excluded.createdAt,
37987
38089
  updatedAt = excluded.updatedAt,
37988
38090
  columnMovedAt = excluded.columnMovedAt,
38091
+ firstExecutionAt = excluded.firstExecutionAt,
38092
+ cumulativeActiveMs = excluded.cumulativeActiveMs,
37989
38093
  executionStartedAt = excluded.executionStartedAt,
37990
38094
  executionCompletedAt = excluded.executionCompletedAt,
37991
38095
  dependencies = excluded.dependencies,
@@ -39643,10 +39747,20 @@ ${newTask.description}
39643
39747
  task.column = toColumn;
39644
39748
  task.columnMovedAt = (/* @__PURE__ */ new Date()).toISOString();
39645
39749
  task.updatedAt = task.columnMovedAt;
39646
- if (toColumn === "in-progress" && !task.executionStartedAt) {
39647
- task.executionStartedAt = task.columnMovedAt;
39750
+ if (fromColumn === "in-progress" && toColumn !== "in-progress") {
39751
+ const segmentStartMs = Date.parse(task.executionStartedAt ?? task.columnMovedAt);
39752
+ const segmentEndMs = Date.parse(task.columnMovedAt);
39753
+ const segmentDeltaMs = Number.isFinite(segmentStartMs) && Number.isFinite(segmentEndMs) ? Math.max(0, segmentEndMs - segmentStartMs) : 0;
39754
+ task.cumulativeActiveMs = Math.max(0, task.cumulativeActiveMs ?? 0) + segmentDeltaMs;
39648
39755
  }
39649
39756
  if (toColumn === "in-progress") {
39757
+ task.cumulativeActiveMs ??= 0;
39758
+ if (!task.firstExecutionAt) {
39759
+ task.firstExecutionAt = task.columnMovedAt;
39760
+ }
39761
+ if (!task.executionStartedAt) {
39762
+ task.executionStartedAt = task.columnMovedAt;
39763
+ }
39650
39764
  task.userPaused = void 0;
39651
39765
  }
39652
39766
  if (toColumn === "done" && !task.executionCompletedAt) {
@@ -40088,6 +40202,16 @@ ${newTask.description}
40088
40202
  } else if (updates.sessionFile !== void 0) {
40089
40203
  task.sessionFile = updates.sessionFile;
40090
40204
  }
40205
+ if (updates.firstExecutionAt === null) {
40206
+ task.firstExecutionAt = void 0;
40207
+ } else if (updates.firstExecutionAt !== void 0) {
40208
+ task.firstExecutionAt = updates.firstExecutionAt;
40209
+ }
40210
+ if (updates.cumulativeActiveMs === null) {
40211
+ task.cumulativeActiveMs = void 0;
40212
+ } else if (updates.cumulativeActiveMs !== void 0) {
40213
+ task.cumulativeActiveMs = updates.cumulativeActiveMs;
40214
+ }
40091
40215
  if (updates.executionStartedAt === null) {
40092
40216
  task.executionStartedAt = void 0;
40093
40217
  } else if (updates.executionStartedAt !== void 0) {
@@ -40219,6 +40343,7 @@ ${newTask.description}
40219
40343
  }
40220
40344
  if (!paused) {
40221
40345
  task.pausedByAgentId = void 0;
40346
+ task.userPaused = void 0;
40222
40347
  }
40223
40348
  if (task.column === "in-progress" || task.column === "in-review") {
40224
40349
  task.status = paused ? "paused" : void 0;
@@ -60257,13 +60382,15 @@ var init_chat_store = __esm({
60257
60382
  whereClauses.push("createdAt < ?");
60258
60383
  params.push(filter.before);
60259
60384
  }
60385
+ const order = filter?.order === "desc" ? "DESC" : "ASC";
60260
60386
  const rows = this.db.prepare(`
60261
60387
  SELECT * FROM chat_room_messages
60262
60388
  WHERE ${whereClauses.join(" AND ")}
60263
- ORDER BY createdAt ASC
60389
+ ORDER BY createdAt ${order}
60264
60390
  LIMIT ? OFFSET ?
60265
60391
  `).all(...params, filter?.limit ?? 100, filter?.offset ?? 0);
60266
- return rows.map((row) => this.rowToRoomMessage(row));
60392
+ const normalizedRows = filter?.order === "desc" ? [...rows].reverse() : rows;
60393
+ return normalizedRows.map((row) => this.rowToRoomMessage(row));
60267
60394
  }
60268
60395
  listRoomMessagesSince(roomId, sinceIso, options) {
60269
60396
  const whereClauses = ["roomId = ?", "createdAt > ?"];
@@ -60544,6 +60671,8 @@ var src_exports = {};
60544
60671
  __export(src_exports, {
60545
60672
  AGENT_PERMISSIONS: () => AGENT_PERMISSIONS,
60546
60673
  AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: () => AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
60674
+ AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES,
60675
+ AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES,
60547
60676
  AGENT_PERMISSION_POLICY_PRESET_IDS: () => AGENT_PERMISSION_POLICY_PRESET_IDS,
60548
60677
  AGENT_PROVISIONING_APPROVAL_MODES: () => AGENT_PROVISIONING_APPROVAL_MODES,
60549
60678
  AGENT_VALID_TRANSITIONS: () => AGENT_VALID_TRANSITIONS,
@@ -61604,6 +61733,7 @@ var init_gating_classifications = __esm({
61604
61733
  "fn_slice_activate",
61605
61734
  "fn_feature_link_task",
61606
61735
  "fn_feature_update",
61736
+ "fn_milestone_update",
61607
61737
  "fn_agent_stop",
61608
61738
  "fn_agent_start"
61609
61739
  ];
@@ -61624,9 +61754,15 @@ var init_gating_classifications = __esm({
61624
61754
  NETWORK_API_TOOLS = /* @__PURE__ */ new Set([
61625
61755
  "fn_research_run",
61626
61756
  "fn_research_cancel",
61627
- "fn_research_retry"
61757
+ "fn_research_retry",
61758
+ "fn_web_fetch"
61759
+ // FN-4603: outbound HTTP fetch should be network-classified.
61760
+ ]);
61761
+ ACTION_GATE_NETWORK_API_TOOLS = /* @__PURE__ */ new Set([
61762
+ "fn_research_run",
61763
+ "fn_web_fetch"
61764
+ // FN-4603: honor network_api approval policy for web fetches.
61628
61765
  ]);
61629
- ACTION_GATE_NETWORK_API_TOOLS = /* @__PURE__ */ new Set(["fn_research_run"]);
61630
61766
  READONLY_FN_TOOLS = /* @__PURE__ */ new Set([
61631
61767
  "fn_task_list",
61632
61768
  "fn_task_show",
@@ -83430,6 +83566,91 @@ var init_auto_recovery = __esm({
83430
83566
  }
83431
83567
  });
83432
83568
 
83569
+ // ../engine/src/restart-recovery-coordinator.ts
83570
+ function hasStepProgress(task) {
83571
+ const steps = Array.isArray(task.steps) ? task.steps : [];
83572
+ return steps.some((step) => step.status === "done" || step.status === "in-progress" || step.status === "skipped");
83573
+ }
83574
+ function isNoTaskDoneFailure(task) {
83575
+ return task.status === "failed" && typeof task.error === "string" && task.error.toLowerCase().includes("without calling fn_task_done");
83576
+ }
83577
+ function findMissingWorktreeSessionPrefix(error) {
83578
+ for (const prefix of MISSING_WORKTREE_SESSION_PREFIXES) {
83579
+ if (error.includes(prefix)) {
83580
+ return prefix;
83581
+ }
83582
+ }
83583
+ return null;
83584
+ }
83585
+ function isMissingWorktreeSessionStartFailure(error) {
83586
+ if (typeof error !== "string") {
83587
+ return false;
83588
+ }
83589
+ return findMissingWorktreeSessionPrefix(error) !== null;
83590
+ }
83591
+ function extractMissingWorktreePathFromSessionStartFailure(error) {
83592
+ if (typeof error !== "string") return null;
83593
+ const prefix = findMissingWorktreeSessionPrefix(error);
83594
+ if (!prefix) return null;
83595
+ const idx = error.indexOf(prefix);
83596
+ const pathPart = error.slice(idx + prefix.length).trim();
83597
+ return pathPart.length > 0 ? pathPart : null;
83598
+ }
83599
+ function isRecoverableMissingWorktreeReviewFailure(task) {
83600
+ return task.column === "in-review" && !task.paused && task.status === "failed" && isMissingWorktreeSessionStartFailure(task.error) && hasStepProgress(task);
83601
+ }
83602
+ var log14, MISSING_WORKTREE_SESSION_PREFIXES, RestartRecoveryCoordinator;
83603
+ var init_restart_recovery_coordinator = __esm({
83604
+ "../engine/src/restart-recovery-coordinator.ts"() {
83605
+ "use strict";
83606
+ init_logger2();
83607
+ log14 = createLogger2("restart-recovery");
83608
+ MISSING_WORKTREE_SESSION_PREFIXES = [
83609
+ "Refusing to start coding agent in missing worktree:",
83610
+ "Refusing to start coding agent in incomplete worktree:",
83611
+ "Refusing to start coding agent in unregistered git worktree:"
83612
+ ];
83613
+ RestartRecoveryCoordinator = class {
83614
+ constructor(store, executor) {
83615
+ this.store = store;
83616
+ this.executor = executor;
83617
+ }
83618
+ async recoverInterruptedRuns() {
83619
+ const allInProgress = await this.store.listTasks({ slim: true, column: "in-progress" });
83620
+ const candidates = allInProgress.filter((task) => task.column === "in-progress" && !task.paused);
83621
+ if (candidates.length === 0) return;
83622
+ let requeued = 0;
83623
+ for (const task of candidates) {
83624
+ if (!this.mustSafeRetry(task)) continue;
83625
+ await this.safeRequeue(task);
83626
+ requeued++;
83627
+ }
83628
+ if (requeued > 0) {
83629
+ log14.log(`Restart recovery requeued ${requeued} interrupted task(s) for safe retry`);
83630
+ }
83631
+ await this.executor.resumeOrphaned();
83632
+ }
83633
+ mustSafeRetry(task) {
83634
+ return isNoTaskDoneFailure(task) && !hasStepProgress(task);
83635
+ }
83636
+ async safeRequeue(task) {
83637
+ await this.store.updateTask(task.id, {
83638
+ status: "stuck-killed",
83639
+ worktree: null,
83640
+ branch: null,
83641
+ sessionFile: null,
83642
+ error: null
83643
+ });
83644
+ await this.store.logEntry(
83645
+ task.id,
83646
+ "Restart recovery: interrupted run had no step progress and no fn_task_done \u2014 requeued to todo for safe retry"
83647
+ );
83648
+ await this.store.moveTask(task.id, "todo");
83649
+ }
83650
+ };
83651
+ }
83652
+ });
83653
+
83433
83654
  // ../engine/src/auto-recovery-handlers/branch-worktree.ts
83434
83655
  import { exec as exec9 } from "node:child_process";
83435
83656
  import { existsSync as existsSync32 } from "node:fs";
@@ -84603,6 +84824,7 @@ var init_executor = __esm({
84603
84824
  init_prompt_layers();
84604
84825
  init_run_audit();
84605
84826
  init_auto_recovery();
84827
+ init_restart_recovery_coordinator();
84606
84828
  init_branch_worktree();
84607
84829
  init_contamination();
84608
84830
  init_file_scope();
@@ -87286,7 +87508,22 @@ ${summary}`,
87286
87508
  this.options.onComplete?.(task);
87287
87509
  } else {
87288
87510
  let taskDoneSessionRetries = 0;
87511
+ let retryAbortedDueToReclaim = false;
87289
87512
  while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
87513
+ const liveTask = await this.store.getTask(task.id);
87514
+ const hasExplicitWorktreeBinding = typeof liveTask.worktree === "string" || liveTask.worktree === null;
87515
+ const hasExplicitBranchBinding = typeof liveTask.branch === "string" || liveTask.branch === null;
87516
+ const worktreeContractIntact = liveTask.column === "in-progress" && !liveTask.paused && (!hasExplicitWorktreeBinding || liveTask.worktree === worktreePath) && (!hasExplicitBranchBinding || typeof liveTask.branch === "string" && liveTask.branch.length > 0);
87517
+ if (!worktreeContractIntact) {
87518
+ const reclaimMessage = `${task.id}: worktree/branch reclaimed during no-fn_task_done retry \u2014 aborting retry and requeueing`;
87519
+ executorLog.log(reclaimMessage);
87520
+ await this.store.logEntry(task.id, reclaimMessage, void 0, this.currentRunContext);
87521
+ this.activeSessions.delete(task.id);
87522
+ this.tokenUsageBaselines.delete(task.id);
87523
+ session.dispose();
87524
+ retryAbortedDueToReclaim = true;
87525
+ break;
87526
+ }
87290
87527
  taskDoneSessionRetries++;
87291
87528
  executorLog.log(
87292
87529
  `\u26A0 ${task.id} finished without fn_task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
@@ -87313,88 +87550,112 @@ ${summary}`,
87313
87550
  this.activeSessions.delete(task.id);
87314
87551
  this.tokenUsageBaselines.delete(task.id);
87315
87552
  session.dispose();
87316
- const { session: retrySession, sessionFile: retrySessionFile } = await createResolvedAgentSession({
87317
- sessionPurpose: "executor",
87318
- runtimeHint: executorRuntimeHint,
87319
- pluginRunner: this.options.pluginRunner,
87320
- cwd: worktreePath,
87321
- systemPrompt: executorSystemPromptFinal,
87322
- systemPromptLayers: executorLayers,
87323
- tools: "coding",
87324
- customTools,
87325
- onText: agentLogger.onText,
87326
- onThinking: agentLogger.onThinking,
87327
- onToolStart: agentLogger.onToolStart,
87328
- onToolEnd: agentLogger.onToolEnd,
87329
- defaultProvider: executorProvider,
87330
- defaultModelId: executorModelId,
87331
- fallbackProvider: executorFallbackProvider,
87332
- fallbackModelId: executorFallbackModelId,
87333
- defaultThinkingLevel: executorThinkingLevel,
87334
- sessionManager: SessionManager2.create(worktreePath),
87335
- taskEnv,
87336
- // Skill selection: use assigned agent skills if available, otherwise role fallback
87337
- ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
87338
- actionGateContext: this.buildActionGateContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
87339
- permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy)
87340
- });
87341
- if (retrySessionFile) {
87342
- this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err) => {
87343
- const msg = err instanceof Error ? err.message : String(err);
87344
- executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
87553
+ let retrySession = null;
87554
+ try {
87555
+ const createdRetrySession = await createResolvedAgentSession({
87556
+ sessionPurpose: "executor",
87557
+ runtimeHint: executorRuntimeHint,
87558
+ pluginRunner: this.options.pluginRunner,
87559
+ cwd: worktreePath,
87560
+ systemPrompt: executorSystemPromptFinal,
87561
+ systemPromptLayers: executorLayers,
87562
+ tools: "coding",
87563
+ customTools,
87564
+ onText: agentLogger.onText,
87565
+ onThinking: agentLogger.onThinking,
87566
+ onToolStart: agentLogger.onToolStart,
87567
+ onToolEnd: agentLogger.onToolEnd,
87568
+ defaultProvider: executorProvider,
87569
+ defaultModelId: executorModelId,
87570
+ fallbackProvider: executorFallbackProvider,
87571
+ fallbackModelId: executorFallbackModelId,
87572
+ defaultThinkingLevel: executorThinkingLevel,
87573
+ sessionManager: SessionManager2.create(worktreePath),
87574
+ taskEnv,
87575
+ // Skill selection: use assigned agent skills if available, otherwise role fallback
87576
+ ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
87577
+ actionGateContext: this.buildActionGateContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
87578
+ permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy)
87345
87579
  });
87580
+ retrySession = createdRetrySession.session;
87581
+ if (createdRetrySession.sessionFile) {
87582
+ this.store.updateTask(task.id, { sessionFile: createdRetrySession.sessionFile }).catch((err) => {
87583
+ const msg = err instanceof Error ? err.message : String(err);
87584
+ executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
87585
+ });
87586
+ }
87587
+ session = retrySession;
87588
+ sessionRef.current = retrySession;
87589
+ this.activeSessions.set(task.id, {
87590
+ session: retrySession,
87591
+ seenSteeringIds,
87592
+ lastResolvedModelProvider: executorProvider,
87593
+ lastResolvedModelId: executorModelId,
87594
+ lastTaskModelProvider: detail.modelProvider,
87595
+ lastTaskModelId: detail.modelId,
87596
+ lastAssignedAgentId: detail.assignedAgentId ?? null
87597
+ });
87598
+ stuckDetector?.trackTask(task.id, retrySession);
87599
+ let retryPrompt;
87600
+ if (pseudoPause.kind !== "none") {
87601
+ const shortMatch = (pseudoPause.matched ?? "").slice(0, 120);
87602
+ retryPrompt = [
87603
+ `Your previous turn ended with a pseudo-pause: "${shortMatch}". This is forbidden.`,
87604
+ "",
87605
+ "Turn-ending rules you violated:",
87606
+ "- You MUST NOT end a turn by asking the user a question, summarizing progress, or requesting permission to continue.",
87607
+ "- Phrases like 'If you want, I can continue', 'Should I proceed?', 'Let me know if...' are FORBIDDEN turn-endings.",
87608
+ "- The user is not watching this conversation. Questions written as prose are ignored.",
87609
+ "- If you genuinely cannot proceed, call fn_task_done with a clear explanation \u2014 never write the blocker as plain prose.",
87610
+ "",
87611
+ "What you must do now:",
87612
+ "1. Review the PROMPT.md steps and identify the next pending step.",
87613
+ "2. Do the work for that step immediately \u2014 call fn_task_update, write code, run tests.",
87614
+ "3. Continue until all steps are done, then call fn_task_done.",
87615
+ "Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.",
87616
+ "",
87617
+ "Original task:",
87618
+ buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
87619
+ ].join("\n");
87620
+ } else {
87621
+ retryPrompt = [
87622
+ "Your previous session ended without calling the fn_task_done tool.",
87623
+ "The task may already be complete \u2014 review the current state of the worktree and either:",
87624
+ "1. If the work is done, call fn_task_done with a summary of what was accomplished.",
87625
+ "2. If there is remaining work, finish it and then call fn_task_done.",
87626
+ "",
87627
+ "Original task:",
87628
+ buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
87629
+ ].join("\n");
87630
+ }
87631
+ stuckDetector?.recordActivity(task.id);
87632
+ await promptWithFallback(retrySession, retryPrompt);
87633
+ checkSessionError(retrySession);
87634
+ await accumulateSessionTokenUsage(this.store, task.id, retrySession, {
87635
+ agentId: task.assignedAgentId ?? void 0,
87636
+ role: "executor"
87637
+ });
87638
+ } catch (retryError) {
87639
+ const retryErrorText = retryError instanceof Error ? retryError.message : String(retryError);
87640
+ if (!isMissingWorktreeSessionStartFailure(retryErrorText)) {
87641
+ throw retryError;
87642
+ }
87643
+ const recoveredPath = extractMissingWorktreePathFromSessionStartFailure(retryErrorText) ?? worktreePath;
87644
+ const reclaimMessage = `${task.id}: no-fn_task_done retry hit missing worktree session-start failure (${recoveredPath}) \u2014 clearing stale metadata and requeueing`;
87645
+ executorLog.log(reclaimMessage);
87646
+ await this.store.logEntry(task.id, reclaimMessage, void 0, this.currentRunContext);
87647
+ await this.store.updateTask(task.id, {
87648
+ sessionFile: null,
87649
+ worktree: null,
87650
+ branch: null,
87651
+ baseCommitSha: null
87652
+ });
87653
+ this.activeSessions.delete(task.id);
87654
+ this.tokenUsageBaselines.delete(task.id);
87655
+ retrySession?.dispose();
87656
+ retryAbortedDueToReclaim = true;
87657
+ break;
87346
87658
  }
87347
- session = retrySession;
87348
- sessionRef.current = retrySession;
87349
- this.activeSessions.set(task.id, {
87350
- session: retrySession,
87351
- seenSteeringIds,
87352
- lastResolvedModelProvider: executorProvider,
87353
- lastResolvedModelId: executorModelId,
87354
- lastTaskModelProvider: detail.modelProvider,
87355
- lastTaskModelId: detail.modelId,
87356
- lastAssignedAgentId: detail.assignedAgentId ?? null
87357
- });
87358
- stuckDetector?.trackTask(task.id, retrySession);
87359
- let retryPrompt;
87360
- if (pseudoPause.kind !== "none") {
87361
- const shortMatch = (pseudoPause.matched ?? "").slice(0, 120);
87362
- retryPrompt = [
87363
- `Your previous turn ended with a pseudo-pause: "${shortMatch}". This is forbidden.`,
87364
- "",
87365
- "Turn-ending rules you violated:",
87366
- "- You MUST NOT end a turn by asking the user a question, summarizing progress, or requesting permission to continue.",
87367
- "- Phrases like 'If you want, I can continue', 'Should I proceed?', 'Let me know if...' are FORBIDDEN turn-endings.",
87368
- "- The user is not watching this conversation. Questions written as prose are ignored.",
87369
- "- If you genuinely cannot proceed, call fn_task_done with a clear explanation \u2014 never write the blocker as plain prose.",
87370
- "",
87371
- "What you must do now:",
87372
- "1. Review the PROMPT.md steps and identify the next pending step.",
87373
- "2. Do the work for that step immediately \u2014 call fn_task_update, write code, run tests.",
87374
- "3. Continue until all steps are done, then call fn_task_done.",
87375
- "Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.",
87376
- "",
87377
- "Original task:",
87378
- buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
87379
- ].join("\n");
87380
- } else {
87381
- retryPrompt = [
87382
- "Your previous session ended without calling the fn_task_done tool.",
87383
- "The task may already be complete \u2014 review the current state of the worktree and either:",
87384
- "1. If the work is done, call fn_task_done with a summary of what was accomplished.",
87385
- "2. If there is remaining work, finish it and then call fn_task_done.",
87386
- "",
87387
- "Original task:",
87388
- buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
87389
- ].join("\n");
87390
- }
87391
- stuckDetector?.recordActivity(task.id);
87392
- await promptWithFallback(retrySession, retryPrompt);
87393
- checkSessionError(retrySession);
87394
- await accumulateSessionTokenUsage(this.store, task.id, retrySession, {
87395
- agentId: task.assignedAgentId ?? void 0,
87396
- role: "executor"
87397
- });
87398
87659
  if (!taskDone) {
87399
87660
  const implicitCheck = await this.store.getTask(task.id);
87400
87661
  if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
@@ -87457,7 +87718,7 @@ ${summary}`,
87457
87718
  } else {
87458
87719
  const priorRequeues = task.taskDoneRetryCount ?? 0;
87459
87720
  const nextRequeueCount = priorRequeues + 1;
87460
- const errorMessage = `Agent finished without calling fn_task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
87721
+ const errorMessage = retryAbortedDueToReclaim ? "Worktree/branch reclaimed during no-fn_task_done retry \u2014 requeueing" : `Agent finished without calling fn_task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
87461
87722
  if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
87462
87723
  await this.store.updateTask(task.id, {
87463
87724
  status: "failed",
@@ -99783,7 +100044,7 @@ function extractJson(raw) {
99783
100044
  if (first >= 0 && last > first) return trimmed.slice(first, last + 1);
99784
100045
  return trimmed;
99785
100046
  }
99786
- var log14, HybridEvaluatorService;
100047
+ var log15, HybridEvaluatorService;
99787
100048
  var init_evaluator = __esm({
99788
100049
  "../engine/src/evaluator.ts"() {
99789
100050
  "use strict";
@@ -99792,7 +100053,7 @@ var init_evaluator = __esm({
99792
100053
  init_eval_followups();
99793
100054
  init_pi();
99794
100055
  init_logger2();
99795
- log14 = createLogger2("evaluator");
100056
+ log15 = createLogger2("evaluator");
99796
100057
  HybridEvaluatorService = class {
99797
100058
  constructor(deps) {
99798
100059
  this.deps = deps;
@@ -99884,7 +100145,7 @@ var init_evaluator = __esm({
99884
100145
  try {
99885
100146
  session.dispose();
99886
100147
  } catch (error) {
99887
- log14.warn(`Evaluator session disposal failed: ${error instanceof Error ? error.message : String(error)}`);
100148
+ log15.warn(`Evaluator session disposal failed: ${error instanceof Error ? error.message : String(error)}`);
99888
100149
  }
99889
100150
  }
99890
100151
  }
@@ -100016,7 +100277,7 @@ function truncateOutput2(stdout, stderr) {
100016
100277
  }
100017
100278
  return combined;
100018
100279
  }
100019
- var log15, FUSION_BINARY_TOKENS, SHELL_METACHARACTERS_REGEX, DEFAULT_TIMEOUT_MS8, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
100280
+ var log16, FUSION_BINARY_TOKENS, SHELL_METACHARACTERS_REGEX, DEFAULT_TIMEOUT_MS8, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
100020
100281
  var init_cron_runner = __esm({
100021
100282
  "../engine/src/cron-runner.ts"() {
100022
100283
  "use strict";
@@ -100025,7 +100286,7 @@ var init_cron_runner = __esm({
100025
100286
  init_shell_utils();
100026
100287
  init_pi();
100027
100288
  init_evaluator();
100028
- log15 = createLogger2("cron-runner");
100289
+ log16 = createLogger2("cron-runner");
100029
100290
  FUSION_BINARY_TOKENS = /* @__PURE__ */ new Set([
100030
100291
  "fn",
100031
100292
  "fusion",
@@ -100066,7 +100327,7 @@ var init_cron_runner = __esm({
100066
100327
  start() {
100067
100328
  if (this.running) return;
100068
100329
  this.running = true;
100069
- log15.log(`Started (poll every ${this.pollIntervalMs / 1e3}s, scope: ${this.scope})`);
100330
+ log16.log(`Started (poll every ${this.pollIntervalMs / 1e3}s, scope: ${this.scope})`);
100070
100331
  void this.tick();
100071
100332
  this.pollInterval = setInterval(() => {
100072
100333
  void this.tick();
@@ -100080,7 +100341,7 @@ var init_cron_runner = __esm({
100080
100341
  clearInterval(this.pollInterval);
100081
100342
  this.pollInterval = null;
100082
100343
  }
100083
- log15.log("Stopped");
100344
+ log16.log("Stopped");
100084
100345
  }
100085
100346
  /**
100086
100347
  * Single poll cycle: find due schedules and execute them.
@@ -100104,29 +100365,29 @@ var init_cron_runner = __esm({
100104
100365
  const executedIds = /* @__PURE__ */ new Set();
100105
100366
  for (const schedule of dueSchedules) {
100106
100367
  if (this.inFlight.has(schedule.id)) {
100107
- log15.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 still running from previous tick`);
100368
+ log16.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 still running from previous tick`);
100108
100369
  continue;
100109
100370
  }
100110
100371
  if (executedIds.has(schedule.id)) {
100111
- log15.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 already executed from another scope this tick`);
100372
+ log16.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 already executed from another scope this tick`);
100112
100373
  continue;
100113
100374
  }
100114
100375
  executedIds.add(schedule.id);
100115
100376
  const scheduleScope = schedule.scope ?? "project";
100116
100377
  if (scheduleScope !== this.scope && this.scope !== "all") {
100117
- log15.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
100378
+ log16.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
100118
100379
  continue;
100119
100380
  }
100120
- log15.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
100381
+ log16.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
100121
100382
  const currentSettings = await this.store.getSettings();
100122
100383
  if (currentSettings.globalPause || currentSettings.enginePaused) {
100123
- log15.log("Pause detected mid-tick \u2014 stopping schedule execution");
100384
+ log16.log("Pause detected mid-tick \u2014 stopping schedule execution");
100124
100385
  break;
100125
100386
  }
100126
100387
  await this.executeSchedule(schedule);
100127
100388
  }
100128
100389
  } catch (err) {
100129
- log15.error(`Tick error: ${err.message}`);
100390
+ log16.error(`Tick error: ${err.message}`);
100130
100391
  } finally {
100131
100392
  this.ticking = false;
100132
100393
  }
@@ -100156,13 +100417,13 @@ var init_cron_runner = __esm({
100156
100417
  try {
100157
100418
  await this.automationStore.recordRun(schedule.id, result);
100158
100419
  } catch (recordErr) {
100159
- log15.error(`Failed to record run for ${schedule.id}: ${recordErr.message}`);
100420
+ log16.error(`Failed to record run for ${schedule.id}: ${recordErr.message}`);
100160
100421
  }
100161
100422
  if (this.onScheduleRunProcessed) {
100162
100423
  try {
100163
100424
  await this.onScheduleRunProcessed(schedule, result);
100164
100425
  } catch (callbackErr) {
100165
- log15.error(
100426
+ log16.error(
100166
100427
  `Post-run callback failed for ${schedule.name} (${schedule.id}): ${callbackErr.message}`
100167
100428
  );
100168
100429
  }
@@ -100173,7 +100434,7 @@ var init_cron_runner = __esm({
100173
100434
  * Execute a legacy single-command schedule.
100174
100435
  */
100175
100436
  async executeLegacyCommand(schedule, startedAt) {
100176
- log15.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
100437
+ log16.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
100177
100438
  if (isInProcessBackupCommand(schedule.command)) {
100178
100439
  return this.executeBackupInProcess(schedule, startedAt);
100179
100440
  }
@@ -100191,7 +100452,7 @@ var init_cron_runner = __esm({
100191
100452
  shell: defaultShell
100192
100453
  });
100193
100454
  const output = truncateOutput2(stdout, stderr);
100194
- log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
100455
+ log16.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
100195
100456
  return {
100196
100457
  success: true,
100197
100458
  output,
@@ -100203,7 +100464,7 @@ var init_cron_runner = __esm({
100203
100464
  const stderr = err.stderr ?? "";
100204
100465
  const output = truncateOutput2(stdout, stderr);
100205
100466
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS8) / 1e3}s` : err.message ?? String(err);
100206
- log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
100467
+ log16.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
100207
100468
  return {
100208
100469
  success: false,
100209
100470
  output,
@@ -100221,9 +100482,9 @@ var init_cron_runner = __esm({
100221
100482
  async executeBackupInProcess(schedule, startedAt) {
100222
100483
  const action = await this.runBackupActionInProcess();
100223
100484
  if (action.success) {
100224
- log15.log(`\u2713 ${schedule.name} completed in-process`);
100485
+ log16.log(`\u2713 ${schedule.name} completed in-process`);
100225
100486
  } else {
100226
- log15.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
100487
+ log16.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
100227
100488
  }
100228
100489
  return {
100229
100490
  success: action.success,
@@ -100236,9 +100497,9 @@ var init_cron_runner = __esm({
100236
100497
  async executeMemoryBackupInProcess(schedule, startedAt) {
100237
100498
  const action = await this.runMemoryBackupActionInProcess();
100238
100499
  if (action.success) {
100239
- log15.log(`\u2713 ${schedule.name} completed in-process`);
100500
+ log16.log(`\u2713 ${schedule.name} completed in-process`);
100240
100501
  } else {
100241
- log15.warn(`\u2717 ${schedule.name} in-process memory backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
100502
+ log16.warn(`\u2717 ${schedule.name} in-process memory backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
100242
100503
  }
100243
100504
  return {
100244
100505
  success: action.success,
@@ -100327,25 +100588,25 @@ var init_cron_runner = __esm({
100327
100588
  */
100328
100589
  async executeSteps(schedule, startedAt) {
100329
100590
  const steps = schedule.steps;
100330
- log15.log(`Executing ${schedule.name} (${schedule.id}): ${steps.length} steps`);
100591
+ log16.log(`Executing ${schedule.name} (${schedule.id}): ${steps.length} steps`);
100331
100592
  const stepResults = [];
100332
100593
  let overallSuccess = true;
100333
100594
  let stoppedEarly = false;
100334
100595
  for (let i = 0; i < steps.length; i++) {
100335
100596
  const step = steps[i];
100336
- log15.log(` Step ${i + 1}/${steps.length}: ${step.name} (${step.type})`);
100597
+ log16.log(` Step ${i + 1}/${steps.length}: ${step.name} (${step.type})`);
100337
100598
  const stepResult = await this.executeStep(schedule, step, i);
100338
100599
  stepResults.push(stepResult);
100339
100600
  if (!stepResult.success) {
100340
100601
  overallSuccess = false;
100341
100602
  if (!step.continueOnFailure) {
100342
- log15.warn(` Step "${step.name}" failed \u2014 stopping execution`);
100603
+ log16.warn(` Step "${step.name}" failed \u2014 stopping execution`);
100343
100604
  stoppedEarly = true;
100344
100605
  break;
100345
100606
  }
100346
- log15.warn(` Step "${step.name}" failed \u2014 continuing (continueOnFailure=true)`);
100607
+ log16.warn(` Step "${step.name}" failed \u2014 continuing (continueOnFailure=true)`);
100347
100608
  } else {
100348
- log15.log(` \u2713 Step "${step.name}" completed`);
100609
+ log16.log(` \u2713 Step "${step.name}" completed`);
100349
100610
  }
100350
100611
  }
100351
100612
  const outputParts = [];
@@ -100358,7 +100619,7 @@ var init_cron_runner = __esm({
100358
100619
  const failedSteps = stepResults.filter((sr) => !sr.success);
100359
100620
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
100360
100621
  const status = overallSuccess ? "\u2713" : "\u2717";
100361
- log15.log(`${status} ${schedule.name}: ${stepResults.length}/${steps.length} steps executed, ${failedSteps.length} failed`);
100622
+ log16.log(`${status} ${schedule.name}: ${stepResults.length}/${steps.length} steps executed, ${failedSteps.length} failed`);
100362
100623
  return {
100363
100624
  success: overallSuccess,
100364
100625
  output,
@@ -100501,8 +100762,8 @@ var init_cron_runner = __esm({
100501
100762
  const modelProvider = step.modelProvider?.trim() || defaultModel.provider;
100502
100763
  const modelId = step.modelId?.trim() || defaultModel.modelId;
100503
100764
  const model = modelProvider && modelId ? `${modelProvider}/${modelId}` : "default";
100504
- log15.log(` AI prompt step "${step.name}" using model: ${model}`);
100505
- log15.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
100765
+ log16.log(` AI prompt step "${step.name}" using model: ${model}`);
100766
+ log16.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
100506
100767
  try {
100507
100768
  const resultPromise = this.aiPromptExecutor(step.prompt, modelProvider, modelId);
100508
100769
  const timeoutPromise = new Promise((_resolve, reject) => {
@@ -100511,7 +100772,7 @@ var init_cron_runner = __esm({
100511
100772
  const response = await Promise.race([resultPromise, timeoutPromise]);
100512
100773
  const responseText = String(response ?? "");
100513
100774
  const output = responseText.length > MAX_OUTPUT_LENGTH ? responseText.slice(0, MAX_OUTPUT_LENGTH) + "\n[output truncated]" : responseText;
100514
- log15.log(` \u2713 AI prompt step "${step.name}" completed (${responseText.length} chars)`);
100775
+ log16.log(` \u2713 AI prompt step "${step.name}" completed (${responseText.length} chars)`);
100515
100776
  return {
100516
100777
  stepId: step.id,
100517
100778
  stepName: step.name,
@@ -100523,7 +100784,7 @@ var init_cron_runner = __esm({
100523
100784
  };
100524
100785
  } catch (err) {
100525
100786
  const errorMessage = err.message ?? String(err);
100526
- log15.warn(` \u2717 AI prompt step "${step.name}" failed: ${errorMessage}`);
100787
+ log16.warn(` \u2717 AI prompt step "${step.name}" failed: ${errorMessage}`);
100527
100788
  return {
100528
100789
  stepId: step.id,
100529
100790
  stepName: step.name,
@@ -100567,7 +100828,7 @@ var init_cron_runner = __esm({
100567
100828
  try {
100568
100829
  const task = await this.store.createTask(taskInput);
100569
100830
  const output = `Created task ${task.id}: ${task.title || task.description.slice(0, 80)}`;
100570
- log15.log(` \u2713 Create-task step "${step.name}" created task ${task.id}`);
100831
+ log16.log(` \u2713 Create-task step "${step.name}" created task ${task.id}`);
100571
100832
  return {
100572
100833
  stepId: step.id,
100573
100834
  stepName: step.name,
@@ -100579,7 +100840,7 @@ var init_cron_runner = __esm({
100579
100840
  };
100580
100841
  } catch (err) {
100581
100842
  const errorMessage = err.message ?? String(err);
100582
- log15.warn(` \u2717 Create-task step "${step.name}" failed: ${errorMessage}`);
100843
+ log16.warn(` \u2717 Create-task step "${step.name}" failed: ${errorMessage}`);
100583
100844
  return {
100584
100845
  stepId: step.id,
100585
100846
  stepName: step.name,
@@ -100621,7 +100882,7 @@ function truncateOutput3(stdout, stderr) {
100621
100882
  }
100622
100883
  return output;
100623
100884
  }
100624
- var import_cron_parser4, log16, execAsync12, DEFAULT_TIMEOUT_MS9, MAX_BUFFER2, MAX_OUTPUT_LENGTH2, MAX_CATCH_UP_INTERVALS, RoutineRunner;
100885
+ var import_cron_parser4, log17, execAsync12, DEFAULT_TIMEOUT_MS9, MAX_BUFFER2, MAX_OUTPUT_LENGTH2, MAX_CATCH_UP_INTERVALS, RoutineRunner;
100625
100886
  var init_routine_runner = __esm({
100626
100887
  "../engine/src/routine-runner.ts"() {
100627
100888
  "use strict";
@@ -100629,7 +100890,7 @@ var init_routine_runner = __esm({
100629
100890
  init_cron_runner();
100630
100891
  init_logger2();
100631
100892
  init_shell_utils();
100632
- log16 = createLogger2("routine-runner");
100893
+ log17 = createLogger2("routine-runner");
100633
100894
  execAsync12 = promisify17(exec14);
100634
100895
  DEFAULT_TIMEOUT_MS9 = 5 * 60 * 1e3;
100635
100896
  MAX_BUFFER2 = 1024 * 1024;
@@ -100666,7 +100927,7 @@ var init_routine_runner = __esm({
100666
100927
  }
100667
100928
  const concurrency = routine.executionPolicy ?? "queue";
100668
100929
  if (concurrency === "reject" && this.inFlightExecutions.has(routineId)) {
100669
- log16.log(`Routine ${routineId} rejected \u2014 already running`);
100930
+ log17.log(`Routine ${routineId} rejected \u2014 already running`);
100670
100931
  return {
100671
100932
  routineId,
100672
100933
  success: false,
@@ -100677,7 +100938,7 @@ var init_routine_runner = __esm({
100677
100938
  };
100678
100939
  }
100679
100940
  if (concurrency === "queue" && this.inFlightExecutions.has(routineId)) {
100680
- log16.log(`Routine ${routineId} queued \u2014 waiting for existing execution`);
100941
+ log17.log(`Routine ${routineId} queued \u2014 waiting for existing execution`);
100681
100942
  const existingResult = await this.inFlightExecutions.get(routineId);
100682
100943
  if (existingResult) {
100683
100944
  await existingResult;
@@ -100725,7 +100986,7 @@ var init_routine_runner = __esm({
100725
100986
  };
100726
100987
  } catch (err) {
100727
100988
  const errorMessage = err instanceof Error ? err.message : String(err);
100728
- log16.error(`Routine ${routineId} execution failed: ${errorMessage}`);
100989
+ log17.error(`Routine ${routineId} execution failed: ${errorMessage}`);
100729
100990
  try {
100730
100991
  await this.options.routineStore.completeRoutineExecution(routineId, {
100731
100992
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -100733,7 +100994,7 @@ var init_routine_runner = __esm({
100733
100994
  error: errorMessage
100734
100995
  });
100735
100996
  } catch (persistError) {
100736
- log16.error(`[${routineId}] Failed to persist error state: ${persistError}`);
100997
+ log17.error(`[${routineId}] Failed to persist error state: ${persistError}`);
100737
100998
  }
100738
100999
  return {
100739
101000
  routineId,
@@ -100995,7 +101256,7 @@ var init_routine_runner = __esm({
100995
101256
  if (missedIntervals.length === 0) {
100996
101257
  return;
100997
101258
  }
100998
- log16.log(`[${routine.id}] Running ${missedIntervals.length} catch-up executions`);
101259
+ log17.log(`[${routine.id}] Running ${missedIntervals.length} catch-up executions`);
100999
101260
  for (const missedInterval of missedIntervals) {
101000
101261
  try {
101001
101262
  await this.executeRoutine(routine.id, "cron", {
@@ -101003,11 +101264,11 @@ var init_routine_runner = __esm({
101003
101264
  missedInterval: missedInterval.toISOString()
101004
101265
  });
101005
101266
  } catch (err) {
101006
- log16.error(`[${routine.id}] Catch-up execution failed: ${err}`);
101267
+ log17.error(`[${routine.id}] Catch-up execution failed: ${err}`);
101007
101268
  }
101008
101269
  }
101009
101270
  } catch (err) {
101010
- log16.error(`[${routine.id}] Error calculating catch-up intervals: ${err}`);
101271
+ log17.error(`[${routine.id}] Error calculating catch-up intervals: ${err}`);
101011
101272
  }
101012
101273
  }
101013
101274
  /**
@@ -101593,91 +101854,6 @@ var init_stuck_task_detector = __esm({
101593
101854
  }
101594
101855
  });
101595
101856
 
101596
- // ../engine/src/restart-recovery-coordinator.ts
101597
- function hasStepProgress(task) {
101598
- const steps = Array.isArray(task.steps) ? task.steps : [];
101599
- return steps.some((step) => step.status === "done" || step.status === "in-progress" || step.status === "skipped");
101600
- }
101601
- function isNoTaskDoneFailure(task) {
101602
- return task.status === "failed" && typeof task.error === "string" && task.error.toLowerCase().includes("without calling fn_task_done");
101603
- }
101604
- function findMissingWorktreeSessionPrefix(error) {
101605
- for (const prefix of MISSING_WORKTREE_SESSION_PREFIXES) {
101606
- if (error.includes(prefix)) {
101607
- return prefix;
101608
- }
101609
- }
101610
- return null;
101611
- }
101612
- function isMissingWorktreeSessionStartFailure(error) {
101613
- if (typeof error !== "string") {
101614
- return false;
101615
- }
101616
- return findMissingWorktreeSessionPrefix(error) !== null;
101617
- }
101618
- function extractMissingWorktreePathFromSessionStartFailure(error) {
101619
- if (typeof error !== "string") return null;
101620
- const prefix = findMissingWorktreeSessionPrefix(error);
101621
- if (!prefix) return null;
101622
- const idx = error.indexOf(prefix);
101623
- const pathPart = error.slice(idx + prefix.length).trim();
101624
- return pathPart.length > 0 ? pathPart : null;
101625
- }
101626
- function isRecoverableMissingWorktreeReviewFailure(task) {
101627
- return task.column === "in-review" && !task.paused && task.status === "failed" && isMissingWorktreeSessionStartFailure(task.error) && hasStepProgress(task);
101628
- }
101629
- var log17, MISSING_WORKTREE_SESSION_PREFIXES, RestartRecoveryCoordinator;
101630
- var init_restart_recovery_coordinator = __esm({
101631
- "../engine/src/restart-recovery-coordinator.ts"() {
101632
- "use strict";
101633
- init_logger2();
101634
- log17 = createLogger2("restart-recovery");
101635
- MISSING_WORKTREE_SESSION_PREFIXES = [
101636
- "Refusing to start coding agent in missing worktree:",
101637
- "Refusing to start coding agent in incomplete worktree:",
101638
- "Refusing to start coding agent in unregistered git worktree:"
101639
- ];
101640
- RestartRecoveryCoordinator = class {
101641
- constructor(store, executor) {
101642
- this.store = store;
101643
- this.executor = executor;
101644
- }
101645
- async recoverInterruptedRuns() {
101646
- const allInProgress = await this.store.listTasks({ slim: true, column: "in-progress" });
101647
- const candidates = allInProgress.filter((task) => task.column === "in-progress" && !task.paused);
101648
- if (candidates.length === 0) return;
101649
- let requeued = 0;
101650
- for (const task of candidates) {
101651
- if (!this.mustSafeRetry(task)) continue;
101652
- await this.safeRequeue(task);
101653
- requeued++;
101654
- }
101655
- if (requeued > 0) {
101656
- log17.log(`Restart recovery requeued ${requeued} interrupted task(s) for safe retry`);
101657
- }
101658
- await this.executor.resumeOrphaned();
101659
- }
101660
- mustSafeRetry(task) {
101661
- return isNoTaskDoneFailure(task) && !hasStepProgress(task);
101662
- }
101663
- async safeRequeue(task) {
101664
- await this.store.updateTask(task.id, {
101665
- status: "stuck-killed",
101666
- worktree: null,
101667
- branch: null,
101668
- sessionFile: null,
101669
- error: null
101670
- });
101671
- await this.store.logEntry(
101672
- task.id,
101673
- "Restart recovery: interrupted run had no step progress and no fn_task_done \u2014 requeued to todo for safe retry"
101674
- );
101675
- await this.store.moveTask(task.id, "todo");
101676
- }
101677
- };
101678
- }
101679
- });
101680
-
101681
101857
  // ../engine/src/self-healing.ts
101682
101858
  import { exec as exec15, execSync as execSync4 } from "node:child_process";
101683
101859
  import { promisify as promisify18 } from "node:util";
@@ -107455,6 +107631,13 @@ var init_in_process_runtime = __esm({
107455
107631
  getMessageStore() {
107456
107632
  return this.messageStore;
107457
107633
  }
107634
+ /**
107635
+ * Get the ChatStore instance (if initialized).
107636
+ * Returns undefined before start() or if initialization fails.
107637
+ */
107638
+ getChatStore() {
107639
+ return this.chatStore;
107640
+ }
107458
107641
  /**
107459
107642
  * Get the project's Scheduler instance.
107460
107643
  * @throws Error if runtime has not been started
@@ -110428,6 +110611,10 @@ ${detail}`
110428
110611
  getMessageStore() {
110429
110612
  return this.runtime.getMessageStore();
110430
110613
  }
110614
+ /** Get the ChatStore (if initialized). Returns undefined before start(). */
110615
+ getChatStore() {
110616
+ return this.runtime.getChatStore();
110617
+ }
110431
110618
  attachChatStore(chatStore) {
110432
110619
  this.notificationService?.attachChatStore(chatStore);
110433
110620
  }
@@ -134270,6 +134457,71 @@ Status: ${updated.status}`
134270
134457
  };
134271
134458
  }
134272
134459
  });
134460
+ pi.registerTool({
134461
+ name: "fn_milestone_update",
134462
+ label: "fn: Update Milestone",
134463
+ description: "Update an existing milestone's title, description, or acceptance criteria (the structured pass/fail bar, distinct from verification's free-form how-to-confirm notes). Partial patches leave untouched fields intact.",
134464
+ promptSnippet: "Update an existing mission milestone",
134465
+ promptGuidelines: [
134466
+ "Use to revise milestone details without re-creating it",
134467
+ "Mission linkage and ordering are preserved",
134468
+ "Provide only the fields you want to change"
134469
+ ],
134470
+ parameters: Type8.Object({
134471
+ id: Type8.String({ description: "Milestone ID to update (e.g., MS-001)" }),
134472
+ title: Type8.Optional(Type8.String({ description: "Updated milestone title" })),
134473
+ description: Type8.Optional(Type8.String({ description: "Updated milestone description" })),
134474
+ acceptanceCriteria: Type8.Optional(
134475
+ Type8.String({ description: "Updated acceptance criteria for completing the milestone" })
134476
+ )
134477
+ }),
134478
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
134479
+ const store = await getStore2(ctx.cwd);
134480
+ const missionStore = store.getMissionStore();
134481
+ const existingMilestone = missionStore.getMilestone(params.id);
134482
+ if (!existingMilestone) {
134483
+ return {
134484
+ content: [{ type: "text", text: `Milestone ${params.id} not found` }],
134485
+ isError: true,
134486
+ details: { error: "Milestone not found" }
134487
+ };
134488
+ }
134489
+ const updates = {};
134490
+ if ("title" in params) {
134491
+ updates.title = params.title?.trim();
134492
+ }
134493
+ if ("description" in params) {
134494
+ updates.description = params.description?.trim();
134495
+ }
134496
+ if ("acceptanceCriteria" in params) {
134497
+ updates.acceptanceCriteria = params.acceptanceCriteria?.trim();
134498
+ }
134499
+ if (Object.keys(updates).length === 0) {
134500
+ return {
134501
+ content: [
134502
+ {
134503
+ type: "text",
134504
+ text: "No fields to update (provide at least one of: title, description, acceptanceCriteria)"
134505
+ }
134506
+ ],
134507
+ isError: true,
134508
+ details: { error: "No fields to update" }
134509
+ };
134510
+ }
134511
+ const milestone = missionStore.updateMilestone(params.id, updates);
134512
+ return {
134513
+ content: [{ type: "text", text: `Updated ${milestone.id}: "${milestone.title}"` }],
134514
+ details: {
134515
+ milestoneId: milestone.id,
134516
+ missionId: milestone.missionId,
134517
+ title: milestone.title,
134518
+ description: milestone.description,
134519
+ acceptanceCriteria: milestone.acceptanceCriteria,
134520
+ status: milestone.status
134521
+ }
134522
+ };
134523
+ }
134524
+ });
134273
134525
  pi.registerTool({
134274
134526
  name: "fn_agent_stop",
134275
134527
  label: "fn: Stop Agent",