@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/bin.js CHANGED
@@ -866,6 +866,7 @@ Your job: help users transform high-level goals into structured mission plans wi
866
866
  - Features should be specific and actionable
867
867
  - ALWAYS include verification/acceptance criteria at every level:
868
868
  - Milestone: "verification" field \u2014 how to confirm this phase is complete (e.g., "All API endpoints return correct responses, integration tests pass")
869
+ - Milestone: optional "acceptanceCriteria" field for explicit milestone-level completion bars (if omitted, acceptance can be auto-derived from child feature criteria/descriptions)
869
870
  - Slice: "verification" field \u2014 how to confirm this work unit is done (e.g., "Auth flow works end-to-end from signup through login")
870
871
  - Feature: "acceptanceCriteria" field \u2014 how to verify this specific deliverable (e.g., "JWT tokens expire after 1 hour and refresh correctly")
871
872
  - Suggest sensible defaults and push for specificity
@@ -879,7 +880,7 @@ For questions:
879
880
  {"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"}]}}
880
881
 
881
882
  For completion (when you have enough information):
882
- {"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"}]}]}]}}`
883
+ {"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"}]}]}]}}`
883
884
  },
884
885
  "ai-refine-system": {
885
886
  key: "ai-refine-system",
@@ -1451,7 +1452,7 @@ function validateMessageMetadata(metadata) {
1451
1452
  throw new Error("metadata.wakeRecipient must be a boolean");
1452
1453
  }
1453
1454
  }
1454
- 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;
1455
+ 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;
1455
1456
  var init_types = __esm({
1456
1457
  "../core/src/types.ts"() {
1457
1458
  "use strict";
@@ -1885,6 +1886,36 @@ Do NOT spend time on nits when no real issues exist.`
1885
1886
  "network_api",
1886
1887
  "task_agent_mutation"
1887
1888
  ];
1889
+ AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES = {
1890
+ git_write: ["git commit", "git push", "git merge", "git branch -d", "git worktree add", "write", "edit"],
1891
+ file_write_delete: ["write", "edit", "fn_task_attach"],
1892
+ command_execution: ["bash (non-git)", "read", "find", "grep", "ls"],
1893
+ network_api: ["fn_research_run (web/research)", "fn_web_fetch"],
1894
+ task_agent_mutation: [
1895
+ "fn_task_create",
1896
+ "fn_delegate_task",
1897
+ "fn_task_import_github",
1898
+ "fn_task_import_github_issue",
1899
+ "fn_spawn_agent",
1900
+ "fn_update_agent_config",
1901
+ "fn_update_identity"
1902
+ ]
1903
+ };
1904
+ AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES = [
1905
+ "fn_send_message",
1906
+ "fn_post_room_message",
1907
+ "fn_read_messages",
1908
+ "fn_task_log",
1909
+ "fn_task_done",
1910
+ "fn_heartbeat_done",
1911
+ "fn_task_document_write",
1912
+ "fn_task_document_read",
1913
+ "fn_memory_search",
1914
+ "fn_memory_get",
1915
+ "fn_memory_append",
1916
+ "fn_read_evaluations",
1917
+ "fn_reflect_on_performance"
1918
+ ];
1888
1919
  AGENT_PROVISIONING_APPROVAL_MODES = ["always", "trusted-only", "never"];
1889
1920
  AGENT_PERMISSION_POLICY_PRESET_IDS = ["unrestricted", "approval-required", "locked-down", "custom"];
1890
1921
  APPROVAL_REQUEST_STATUSES = ["pending", "approved", "denied", "completed"];
@@ -3727,7 +3758,7 @@ var init_db = __esm({
3727
3758
  DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
3728
3759
  DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
3729
3760
  DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
3730
- SCHEMA_VERSION = 79;
3761
+ SCHEMA_VERSION = 81;
3731
3762
  SCHEMA_SQL = `
3732
3763
  -- Tasks table with JSON columns for nested data
3733
3764
  CREATE TABLE IF NOT EXISTS tasks (
@@ -3782,6 +3813,8 @@ CREATE TABLE IF NOT EXISTS tasks (
3782
3813
  createdAt TEXT NOT NULL,
3783
3814
  updatedAt TEXT NOT NULL,
3784
3815
  columnMovedAt TEXT,
3816
+ firstExecutionAt TEXT,
3817
+ cumulativeActiveMs INTEGER,
3785
3818
  executionStartedAt TEXT,
3786
3819
  executionCompletedAt TEXT,
3787
3820
  -- JSON columns for nested arrays/objects
@@ -4249,6 +4282,7 @@ CREATE TABLE IF NOT EXISTS milestones (
4249
4282
  orderIndex INTEGER NOT NULL,
4250
4283
  interviewState TEXT NOT NULL,
4251
4284
  dependencies TEXT DEFAULT '[]',
4285
+ acceptanceCriteria TEXT,
4252
4286
  createdAt TEXT NOT NULL,
4253
4287
  updatedAt TEXT NOT NULL,
4254
4288
  FOREIGN KEY (missionId) REFERENCES missions(id) ON DELETE CASCADE
@@ -6369,6 +6403,25 @@ This means a caller passed a .fusion directory where a project root was expected
6369
6403
  this.addColumnIfMissing("tasks", "overlapBlockedBy", "TEXT");
6370
6404
  });
6371
6405
  }
6406
+ if (version < 80) {
6407
+ this.applyMigration(80, () => {
6408
+ this.addColumnIfMissing("milestones", "acceptanceCriteria", "TEXT");
6409
+ });
6410
+ }
6411
+ if (version < 81) {
6412
+ this.applyMigration(81, () => {
6413
+ this.addColumnIfMissing("tasks", "firstExecutionAt", "TEXT");
6414
+ this.addColumnIfMissing("tasks", "cumulativeActiveMs", "INTEGER");
6415
+ if (this.hasColumn("tasks", "executionStartedAt")) {
6416
+ this.db.prepare(
6417
+ `UPDATE tasks
6418
+ SET firstExecutionAt = executionStartedAt
6419
+ WHERE firstExecutionAt IS NULL
6420
+ AND executionStartedAt IS NOT NULL`
6421
+ ).run();
6422
+ }
6423
+ });
6424
+ }
6372
6425
  }
6373
6426
  /**
6374
6427
  * Run a single migration step inside a transaction and bump the version.
@@ -10611,6 +10664,21 @@ var init_db_migrate = __esm({
10611
10664
 
10612
10665
  // ../core/src/mission-store.ts
10613
10666
  import { EventEmitter as EventEmitter4 } from "node:events";
10667
+ function deriveMilestoneAcceptanceCriteriaFromFeatures(features) {
10668
+ const lines = features.map((feature) => {
10669
+ const acceptance = feature.acceptanceCriteria?.trim();
10670
+ const description = feature.description?.trim();
10671
+ const text = acceptance && acceptance.length > 0 ? acceptance : description && description.length > 0 ? description : void 0;
10672
+ if (!text) {
10673
+ return void 0;
10674
+ }
10675
+ return `- ${feature.title}: ${text}`;
10676
+ }).filter((line) => Boolean(line));
10677
+ if (lines.length === 0) {
10678
+ return void 0;
10679
+ }
10680
+ return lines.join("\n");
10681
+ }
10614
10682
  var DEFAULT_IMPLEMENTATION_RETRY_BUDGET, MissionStore;
10615
10683
  var init_mission_store = __esm({
10616
10684
  "../core/src/mission-store.ts"() {
@@ -10672,6 +10740,7 @@ var init_mission_store = __esm({
10672
10740
  dependencies: fromJson(row.dependencies) || [],
10673
10741
  planningNotes: row.planningNotes || void 0,
10674
10742
  verification: row.verification || void 0,
10743
+ acceptanceCriteria: row.acceptanceCriteria || void 0,
10675
10744
  validationState: row.validationState || "not_started",
10676
10745
  createdAt: row.createdAt,
10677
10746
  updatedAt: row.updatedAt
@@ -11384,13 +11453,14 @@ var init_mission_store = __esm({
11384
11453
  dependencies: input.dependencies || [],
11385
11454
  planningNotes: input.planningNotes,
11386
11455
  verification: input.verification,
11456
+ acceptanceCriteria: input.acceptanceCriteria,
11387
11457
  validationState: "not_started",
11388
11458
  createdAt: now,
11389
11459
  updatedAt: now
11390
11460
  };
11391
11461
  this.db.prepare(`
11392
- INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
11393
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11462
+ INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, acceptanceCriteria, validationState, createdAt, updatedAt)
11463
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11394
11464
  `).run(
11395
11465
  milestone.id,
11396
11466
  milestone.missionId,
@@ -11402,6 +11472,7 @@ var init_mission_store = __esm({
11402
11472
  toJson(milestone.dependencies),
11403
11473
  milestone.planningNotes ?? null,
11404
11474
  milestone.verification ?? null,
11475
+ milestone.acceptanceCriteria ?? null,
11405
11476
  milestone.validationState,
11406
11477
  milestone.createdAt,
11407
11478
  milestone.updatedAt
@@ -11465,6 +11536,7 @@ var init_mission_store = __esm({
11465
11536
  dependencies = ?,
11466
11537
  planningNotes = ?,
11467
11538
  verification = ?,
11539
+ acceptanceCriteria = ?,
11468
11540
  validationState = ?,
11469
11541
  updatedAt = ?
11470
11542
  WHERE id = ?
@@ -11477,6 +11549,7 @@ var init_mission_store = __esm({
11477
11549
  toJson(updated.dependencies),
11478
11550
  updated.planningNotes ?? null,
11479
11551
  updated.verification ?? null,
11552
+ updated.acceptanceCriteria ?? null,
11480
11553
  updated.validationState || "not_started",
11481
11554
  updated.updatedAt,
11482
11555
  updated.id
@@ -11540,6 +11613,21 @@ var init_mission_store = __esm({
11540
11613
  updateMilestoneInterviewState(id, state) {
11541
11614
  return this.updateMilestone(id, { interviewState: state });
11542
11615
  }
11616
+ applyDerivedMilestoneAcceptanceCriteria(milestoneId) {
11617
+ const milestone = this.getMilestone(milestoneId);
11618
+ if (!milestone) {
11619
+ throw new Error(`Milestone ${milestoneId} not found`);
11620
+ }
11621
+ if (milestone.acceptanceCriteria?.trim()) {
11622
+ return milestone;
11623
+ }
11624
+ const features = this.listSlices(milestoneId).flatMap((slice) => this.listFeatures(slice.id));
11625
+ const derivedAcceptanceCriteria = deriveMilestoneAcceptanceCriteriaFromFeatures(features);
11626
+ if (!derivedAcceptanceCriteria) {
11627
+ return milestone;
11628
+ }
11629
+ return this.updateMilestone(milestoneId, { acceptanceCriteria: derivedAcceptanceCriteria });
11630
+ }
11543
11631
  // ── Slice Operations ───────────────────────────────────────────────
11544
11632
  /**
11545
11633
  * Add a slice to a milestone.
@@ -11811,6 +11899,7 @@ var init_mission_store = __esm({
11811
11899
  this.db.bumpLastModified();
11812
11900
  this.emit("feature:created", feature);
11813
11901
  this.recomputeSliceStatus(sliceId);
11902
+ this.applyDerivedMilestoneAcceptanceCriteria(slice.milestoneId);
11814
11903
  return feature;
11815
11904
  }
11816
11905
  /**
@@ -13180,11 +13269,11 @@ ${feature.acceptanceCriteria}`);
13180
13269
  applied++;
13181
13270
  }
13182
13271
  for (const milestone of snapshot.payload.milestones) {
13183
- this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, validationState, createdAt, updatedAt)
13184
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13272
+ this.db.prepare(`INSERT INTO milestones (id, missionId, title, description, status, orderIndex, interviewState, dependencies, planningNotes, verification, acceptanceCriteria, validationState, createdAt, updatedAt)
13273
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
13185
13274
  ON CONFLICT(id) DO UPDATE SET title=excluded.title, description=excluded.description, status=excluded.status, orderIndex=excluded.orderIndex,
13186
13275
  interviewState=excluded.interviewState, dependencies=excluded.dependencies, planningNotes=excluded.planningNotes, verification=excluded.verification,
13187
- validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
13276
+ acceptanceCriteria=excluded.acceptanceCriteria, validationState=excluded.validationState, updatedAt=excluded.updatedAt`).run(
13188
13277
  milestone.id,
13189
13278
  milestone.missionId,
13190
13279
  milestone.title,
@@ -13195,6 +13284,7 @@ ${feature.acceptanceCriteria}`);
13195
13284
  toJsonNullable(milestone.dependencies),
13196
13285
  milestone.planningNotes ?? null,
13197
13286
  milestone.verification ?? null,
13287
+ milestone.acceptanceCriteria ?? null,
13198
13288
  milestone.validationState ?? null,
13199
13289
  milestone.createdAt,
13200
13290
  milestone.updatedAt
@@ -37226,6 +37316,8 @@ var init_store = __esm({
37226
37316
  createdAt: row.createdAt,
37227
37317
  updatedAt: row.updatedAt,
37228
37318
  columnMovedAt: row.columnMovedAt || void 0,
37319
+ firstExecutionAt: row.firstExecutionAt || void 0,
37320
+ cumulativeActiveMs: row.cumulativeActiveMs ?? void 0,
37229
37321
  executionStartedAt: row.executionStartedAt || void 0,
37230
37322
  executionCompletedAt: row.executionCompletedAt || void 0,
37231
37323
  dependencies: fromJson(row.dependencies) || [],
@@ -37353,6 +37445,8 @@ var init_store = __esm({
37353
37445
  createdAt: entry.createdAt,
37354
37446
  updatedAt: entry.updatedAt,
37355
37447
  columnMovedAt: entry.columnMovedAt,
37448
+ firstExecutionAt: entry.firstExecutionAt,
37449
+ cumulativeActiveMs: entry.cumulativeActiveMs,
37356
37450
  executionStartedAt: entry.executionStartedAt,
37357
37451
  executionCompletedAt: entry.executionCompletedAt,
37358
37452
  modelPresetId: entry.modelPresetId,
@@ -37460,6 +37554,8 @@ ${recentText}` : void 0
37460
37554
  createdAt: task.createdAt,
37461
37555
  updatedAt: task.updatedAt,
37462
37556
  columnMovedAt: task.columnMovedAt,
37557
+ firstExecutionAt: task.firstExecutionAt,
37558
+ cumulativeActiveMs: task.cumulativeActiveMs,
37463
37559
  executionStartedAt: task.executionStartedAt,
37464
37560
  executionCompletedAt: task.executionCompletedAt,
37465
37561
  archivedAt,
@@ -37577,6 +37673,8 @@ ${recentText}` : void 0
37577
37673
  "createdAt",
37578
37674
  "updatedAt",
37579
37675
  "columnMovedAt",
37676
+ "firstExecutionAt",
37677
+ "cumulativeActiveMs",
37580
37678
  "executionStartedAt",
37581
37679
  "executionCompletedAt",
37582
37680
  "dependencies",
@@ -37713,6 +37811,8 @@ ${outcome}`;
37713
37811
  "createdAt",
37714
37812
  "updatedAt",
37715
37813
  "columnMovedAt",
37814
+ "firstExecutionAt",
37815
+ "cumulativeActiveMs",
37716
37816
  "executionStartedAt",
37717
37817
  "executionCompletedAt",
37718
37818
  "dependencies",
@@ -37838,6 +37938,8 @@ ${outcome}`;
37838
37938
  task.createdAt,
37839
37939
  task.updatedAt,
37840
37940
  task.columnMovedAt ?? null,
37941
+ task.firstExecutionAt ?? null,
37942
+ task.cumulativeActiveMs ?? null,
37841
37943
  task.executionStartedAt ?? null,
37842
37944
  task.executionCompletedAt ?? null,
37843
37945
  toJson(task.dependencies || []),
@@ -37902,7 +38004,7 @@ ${outcome}`;
37902
38004
  workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount, nextRecoveryAt, error,
37903
38005
  summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
37904
38006
  tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
37905
- executionStartedAt, executionCompletedAt,
38007
+ firstExecutionAt, cumulativeActiveMs, executionStartedAt, executionCompletedAt,
37906
38008
  dependencies, steps, log, attachments, steeringComments,
37907
38009
  comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
37908
38010
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
@@ -37926,7 +38028,7 @@ ${outcome}`;
37926
38028
  workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount, nextRecoveryAt, error,
37927
38029
  summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
37928
38030
  tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
37929
- executionStartedAt, executionCompletedAt,
38031
+ firstExecutionAt, cumulativeActiveMs, executionStartedAt, executionCompletedAt,
37930
38032
  dependencies, steps, log, attachments, steeringComments,
37931
38033
  comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
37932
38034
  sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
@@ -37988,6 +38090,8 @@ ${outcome}`;
37988
38090
  createdAt = excluded.createdAt,
37989
38091
  updatedAt = excluded.updatedAt,
37990
38092
  columnMovedAt = excluded.columnMovedAt,
38093
+ firstExecutionAt = excluded.firstExecutionAt,
38094
+ cumulativeActiveMs = excluded.cumulativeActiveMs,
37991
38095
  executionStartedAt = excluded.executionStartedAt,
37992
38096
  executionCompletedAt = excluded.executionCompletedAt,
37993
38097
  dependencies = excluded.dependencies,
@@ -39645,10 +39749,20 @@ ${newTask.description}
39645
39749
  task.column = toColumn;
39646
39750
  task.columnMovedAt = (/* @__PURE__ */ new Date()).toISOString();
39647
39751
  task.updatedAt = task.columnMovedAt;
39648
- if (toColumn === "in-progress" && !task.executionStartedAt) {
39649
- task.executionStartedAt = task.columnMovedAt;
39752
+ if (fromColumn === "in-progress" && toColumn !== "in-progress") {
39753
+ const segmentStartMs = Date.parse(task.executionStartedAt ?? task.columnMovedAt);
39754
+ const segmentEndMs = Date.parse(task.columnMovedAt);
39755
+ const segmentDeltaMs = Number.isFinite(segmentStartMs) && Number.isFinite(segmentEndMs) ? Math.max(0, segmentEndMs - segmentStartMs) : 0;
39756
+ task.cumulativeActiveMs = Math.max(0, task.cumulativeActiveMs ?? 0) + segmentDeltaMs;
39650
39757
  }
39651
39758
  if (toColumn === "in-progress") {
39759
+ task.cumulativeActiveMs ??= 0;
39760
+ if (!task.firstExecutionAt) {
39761
+ task.firstExecutionAt = task.columnMovedAt;
39762
+ }
39763
+ if (!task.executionStartedAt) {
39764
+ task.executionStartedAt = task.columnMovedAt;
39765
+ }
39652
39766
  task.userPaused = void 0;
39653
39767
  }
39654
39768
  if (toColumn === "done" && !task.executionCompletedAt) {
@@ -40090,6 +40204,16 @@ ${newTask.description}
40090
40204
  } else if (updates.sessionFile !== void 0) {
40091
40205
  task.sessionFile = updates.sessionFile;
40092
40206
  }
40207
+ if (updates.firstExecutionAt === null) {
40208
+ task.firstExecutionAt = void 0;
40209
+ } else if (updates.firstExecutionAt !== void 0) {
40210
+ task.firstExecutionAt = updates.firstExecutionAt;
40211
+ }
40212
+ if (updates.cumulativeActiveMs === null) {
40213
+ task.cumulativeActiveMs = void 0;
40214
+ } else if (updates.cumulativeActiveMs !== void 0) {
40215
+ task.cumulativeActiveMs = updates.cumulativeActiveMs;
40216
+ }
40093
40217
  if (updates.executionStartedAt === null) {
40094
40218
  task.executionStartedAt = void 0;
40095
40219
  } else if (updates.executionStartedAt !== void 0) {
@@ -40221,6 +40345,7 @@ ${newTask.description}
40221
40345
  }
40222
40346
  if (!paused) {
40223
40347
  task.pausedByAgentId = void 0;
40348
+ task.userPaused = void 0;
40224
40349
  }
40225
40350
  if (task.column === "in-progress" || task.column === "in-review") {
40226
40351
  task.status = paused ? "paused" : void 0;
@@ -60259,13 +60384,15 @@ var init_chat_store = __esm({
60259
60384
  whereClauses.push("createdAt < ?");
60260
60385
  params.push(filter2.before);
60261
60386
  }
60387
+ const order = filter2?.order === "desc" ? "DESC" : "ASC";
60262
60388
  const rows = this.db.prepare(`
60263
60389
  SELECT * FROM chat_room_messages
60264
60390
  WHERE ${whereClauses.join(" AND ")}
60265
- ORDER BY createdAt ASC
60391
+ ORDER BY createdAt ${order}
60266
60392
  LIMIT ? OFFSET ?
60267
60393
  `).all(...params, filter2?.limit ?? 100, filter2?.offset ?? 0);
60268
- return rows.map((row) => this.rowToRoomMessage(row));
60394
+ const normalizedRows = filter2?.order === "desc" ? [...rows].reverse() : rows;
60395
+ return normalizedRows.map((row) => this.rowToRoomMessage(row));
60269
60396
  }
60270
60397
  listRoomMessagesSince(roomId, sinceIso, options) {
60271
60398
  const whereClauses = ["roomId = ?", "createdAt > ?"];
@@ -60546,6 +60673,8 @@ var src_exports = {};
60546
60673
  __export(src_exports, {
60547
60674
  AGENT_PERMISSIONS: () => AGENT_PERMISSIONS,
60548
60675
  AGENT_PERMISSION_POLICY_ACTION_CATEGORIES: () => AGENT_PERMISSION_POLICY_ACTION_CATEGORIES,
60676
+ AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_CATEGORY_TOOL_EXAMPLES,
60677
+ AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES: () => AGENT_PERMISSION_POLICY_EXEMPT_TOOL_EXAMPLES,
60549
60678
  AGENT_PERMISSION_POLICY_PRESET_IDS: () => AGENT_PERMISSION_POLICY_PRESET_IDS,
60550
60679
  AGENT_PROVISIONING_APPROVAL_MODES: () => AGENT_PROVISIONING_APPROVAL_MODES,
60551
60680
  AGENT_VALID_TRANSITIONS: () => AGENT_VALID_TRANSITIONS,
@@ -64764,6 +64893,7 @@ var init_gating_classifications = __esm({
64764
64893
  "fn_slice_activate",
64765
64894
  "fn_feature_link_task",
64766
64895
  "fn_feature_update",
64896
+ "fn_milestone_update",
64767
64897
  "fn_agent_stop",
64768
64898
  "fn_agent_start"
64769
64899
  ];
@@ -64784,9 +64914,15 @@ var init_gating_classifications = __esm({
64784
64914
  NETWORK_API_TOOLS = /* @__PURE__ */ new Set([
64785
64915
  "fn_research_run",
64786
64916
  "fn_research_cancel",
64787
- "fn_research_retry"
64917
+ "fn_research_retry",
64918
+ "fn_web_fetch"
64919
+ // FN-4603: outbound HTTP fetch should be network-classified.
64920
+ ]);
64921
+ ACTION_GATE_NETWORK_API_TOOLS = /* @__PURE__ */ new Set([
64922
+ "fn_research_run",
64923
+ "fn_web_fetch"
64924
+ // FN-4603: honor network_api approval policy for web fetches.
64788
64925
  ]);
64789
- ACTION_GATE_NETWORK_API_TOOLS = /* @__PURE__ */ new Set(["fn_research_run"]);
64790
64926
  READONLY_FN_TOOLS = /* @__PURE__ */ new Set([
64791
64927
  "fn_task_list",
64792
64928
  "fn_task_show",
@@ -86590,6 +86726,91 @@ var init_auto_recovery = __esm({
86590
86726
  }
86591
86727
  });
86592
86728
 
86729
+ // ../engine/src/restart-recovery-coordinator.ts
86730
+ function hasStepProgress(task) {
86731
+ const steps = Array.isArray(task.steps) ? task.steps : [];
86732
+ return steps.some((step) => step.status === "done" || step.status === "in-progress" || step.status === "skipped");
86733
+ }
86734
+ function isNoTaskDoneFailure(task) {
86735
+ return task.status === "failed" && typeof task.error === "string" && task.error.toLowerCase().includes("without calling fn_task_done");
86736
+ }
86737
+ function findMissingWorktreeSessionPrefix(error) {
86738
+ for (const prefix of MISSING_WORKTREE_SESSION_PREFIXES) {
86739
+ if (error.includes(prefix)) {
86740
+ return prefix;
86741
+ }
86742
+ }
86743
+ return null;
86744
+ }
86745
+ function isMissingWorktreeSessionStartFailure(error) {
86746
+ if (typeof error !== "string") {
86747
+ return false;
86748
+ }
86749
+ return findMissingWorktreeSessionPrefix(error) !== null;
86750
+ }
86751
+ function extractMissingWorktreePathFromSessionStartFailure(error) {
86752
+ if (typeof error !== "string") return null;
86753
+ const prefix = findMissingWorktreeSessionPrefix(error);
86754
+ if (!prefix) return null;
86755
+ const idx = error.indexOf(prefix);
86756
+ const pathPart = error.slice(idx + prefix.length).trim();
86757
+ return pathPart.length > 0 ? pathPart : null;
86758
+ }
86759
+ function isRecoverableMissingWorktreeReviewFailure(task) {
86760
+ return task.column === "in-review" && !task.paused && task.status === "failed" && isMissingWorktreeSessionStartFailure(task.error) && hasStepProgress(task);
86761
+ }
86762
+ var log14, MISSING_WORKTREE_SESSION_PREFIXES, RestartRecoveryCoordinator;
86763
+ var init_restart_recovery_coordinator = __esm({
86764
+ "../engine/src/restart-recovery-coordinator.ts"() {
86765
+ "use strict";
86766
+ init_logger2();
86767
+ log14 = createLogger2("restart-recovery");
86768
+ MISSING_WORKTREE_SESSION_PREFIXES = [
86769
+ "Refusing to start coding agent in missing worktree:",
86770
+ "Refusing to start coding agent in incomplete worktree:",
86771
+ "Refusing to start coding agent in unregistered git worktree:"
86772
+ ];
86773
+ RestartRecoveryCoordinator = class {
86774
+ constructor(store, executor) {
86775
+ this.store = store;
86776
+ this.executor = executor;
86777
+ }
86778
+ async recoverInterruptedRuns() {
86779
+ const allInProgress = await this.store.listTasks({ slim: true, column: "in-progress" });
86780
+ const candidates = allInProgress.filter((task) => task.column === "in-progress" && !task.paused);
86781
+ if (candidates.length === 0) return;
86782
+ let requeued = 0;
86783
+ for (const task of candidates) {
86784
+ if (!this.mustSafeRetry(task)) continue;
86785
+ await this.safeRequeue(task);
86786
+ requeued++;
86787
+ }
86788
+ if (requeued > 0) {
86789
+ log14.log(`Restart recovery requeued ${requeued} interrupted task(s) for safe retry`);
86790
+ }
86791
+ await this.executor.resumeOrphaned();
86792
+ }
86793
+ mustSafeRetry(task) {
86794
+ return isNoTaskDoneFailure(task) && !hasStepProgress(task);
86795
+ }
86796
+ async safeRequeue(task) {
86797
+ await this.store.updateTask(task.id, {
86798
+ status: "stuck-killed",
86799
+ worktree: null,
86800
+ branch: null,
86801
+ sessionFile: null,
86802
+ error: null
86803
+ });
86804
+ await this.store.logEntry(
86805
+ task.id,
86806
+ "Restart recovery: interrupted run had no step progress and no fn_task_done \u2014 requeued to todo for safe retry"
86807
+ );
86808
+ await this.store.moveTask(task.id, "todo");
86809
+ }
86810
+ };
86811
+ }
86812
+ });
86813
+
86593
86814
  // ../engine/src/auto-recovery-handlers/branch-worktree.ts
86594
86815
  import { exec as exec9 } from "node:child_process";
86595
86816
  import { existsSync as existsSync32 } from "node:fs";
@@ -87763,6 +87984,7 @@ var init_executor = __esm({
87763
87984
  init_prompt_layers();
87764
87985
  init_run_audit();
87765
87986
  init_auto_recovery();
87987
+ init_restart_recovery_coordinator();
87766
87988
  init_branch_worktree();
87767
87989
  init_contamination();
87768
87990
  init_file_scope();
@@ -90446,7 +90668,22 @@ ${summary}`,
90446
90668
  this.options.onComplete?.(task);
90447
90669
  } else {
90448
90670
  let taskDoneSessionRetries = 0;
90671
+ let retryAbortedDueToReclaim = false;
90449
90672
  while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
90673
+ const liveTask = await this.store.getTask(task.id);
90674
+ const hasExplicitWorktreeBinding = typeof liveTask.worktree === "string" || liveTask.worktree === null;
90675
+ const hasExplicitBranchBinding = typeof liveTask.branch === "string" || liveTask.branch === null;
90676
+ const worktreeContractIntact = liveTask.column === "in-progress" && !liveTask.paused && (!hasExplicitWorktreeBinding || liveTask.worktree === worktreePath) && (!hasExplicitBranchBinding || typeof liveTask.branch === "string" && liveTask.branch.length > 0);
90677
+ if (!worktreeContractIntact) {
90678
+ const reclaimMessage = `${task.id}: worktree/branch reclaimed during no-fn_task_done retry \u2014 aborting retry and requeueing`;
90679
+ executorLog.log(reclaimMessage);
90680
+ await this.store.logEntry(task.id, reclaimMessage, void 0, this.currentRunContext);
90681
+ this.activeSessions.delete(task.id);
90682
+ this.tokenUsageBaselines.delete(task.id);
90683
+ session.dispose();
90684
+ retryAbortedDueToReclaim = true;
90685
+ break;
90686
+ }
90450
90687
  taskDoneSessionRetries++;
90451
90688
  executorLog.log(
90452
90689
  `\u26A0 ${task.id} finished without fn_task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
@@ -90473,88 +90710,112 @@ ${summary}`,
90473
90710
  this.activeSessions.delete(task.id);
90474
90711
  this.tokenUsageBaselines.delete(task.id);
90475
90712
  session.dispose();
90476
- const { session: retrySession2, sessionFile: retrySessionFile } = await createResolvedAgentSession({
90477
- sessionPurpose: "executor",
90478
- runtimeHint: executorRuntimeHint,
90479
- pluginRunner: this.options.pluginRunner,
90480
- cwd: worktreePath,
90481
- systemPrompt: executorSystemPromptFinal,
90482
- systemPromptLayers: executorLayers,
90483
- tools: "coding",
90484
- customTools,
90485
- onText: agentLogger.onText,
90486
- onThinking: agentLogger.onThinking,
90487
- onToolStart: agentLogger.onToolStart,
90488
- onToolEnd: agentLogger.onToolEnd,
90489
- defaultProvider: executorProvider,
90490
- defaultModelId: executorModelId,
90491
- fallbackProvider: executorFallbackProvider,
90492
- fallbackModelId: executorFallbackModelId,
90493
- defaultThinkingLevel: executorThinkingLevel,
90494
- sessionManager: SessionManager2.create(worktreePath),
90495
- taskEnv,
90496
- // Skill selection: use assigned agent skills if available, otherwise role fallback
90497
- ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
90498
- actionGateContext: this.buildActionGateContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
90499
- permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy)
90500
- });
90501
- if (retrySessionFile) {
90502
- this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err) => {
90503
- const msg = err instanceof Error ? err.message : String(err);
90504
- executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
90713
+ let retrySession2 = null;
90714
+ try {
90715
+ const createdRetrySession = await createResolvedAgentSession({
90716
+ sessionPurpose: "executor",
90717
+ runtimeHint: executorRuntimeHint,
90718
+ pluginRunner: this.options.pluginRunner,
90719
+ cwd: worktreePath,
90720
+ systemPrompt: executorSystemPromptFinal,
90721
+ systemPromptLayers: executorLayers,
90722
+ tools: "coding",
90723
+ customTools,
90724
+ onText: agentLogger.onText,
90725
+ onThinking: agentLogger.onThinking,
90726
+ onToolStart: agentLogger.onToolStart,
90727
+ onToolEnd: agentLogger.onToolEnd,
90728
+ defaultProvider: executorProvider,
90729
+ defaultModelId: executorModelId,
90730
+ fallbackProvider: executorFallbackProvider,
90731
+ fallbackModelId: executorFallbackModelId,
90732
+ defaultThinkingLevel: executorThinkingLevel,
90733
+ sessionManager: SessionManager2.create(worktreePath),
90734
+ taskEnv,
90735
+ // Skill selection: use assigned agent skills if available, otherwise role fallback
90736
+ ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
90737
+ actionGateContext: this.buildActionGateContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy),
90738
+ permanentAgentGating: this.buildPermanentAgentGatingContext(task.id, assignedAgent, settings.defaultAgentPermissionPolicy)
90505
90739
  });
90740
+ retrySession2 = createdRetrySession.session;
90741
+ if (createdRetrySession.sessionFile) {
90742
+ this.store.updateTask(task.id, { sessionFile: createdRetrySession.sessionFile }).catch((err) => {
90743
+ const msg = err instanceof Error ? err.message : String(err);
90744
+ executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
90745
+ });
90746
+ }
90747
+ session = retrySession2;
90748
+ sessionRef.current = retrySession2;
90749
+ this.activeSessions.set(task.id, {
90750
+ session: retrySession2,
90751
+ seenSteeringIds,
90752
+ lastResolvedModelProvider: executorProvider,
90753
+ lastResolvedModelId: executorModelId,
90754
+ lastTaskModelProvider: detail.modelProvider,
90755
+ lastTaskModelId: detail.modelId,
90756
+ lastAssignedAgentId: detail.assignedAgentId ?? null
90757
+ });
90758
+ stuckDetector?.trackTask(task.id, retrySession2);
90759
+ let retryPrompt;
90760
+ if (pseudoPause.kind !== "none") {
90761
+ const shortMatch = (pseudoPause.matched ?? "").slice(0, 120);
90762
+ retryPrompt = [
90763
+ `Your previous turn ended with a pseudo-pause: "${shortMatch}". This is forbidden.`,
90764
+ "",
90765
+ "Turn-ending rules you violated:",
90766
+ "- You MUST NOT end a turn by asking the user a question, summarizing progress, or requesting permission to continue.",
90767
+ "- Phrases like 'If you want, I can continue', 'Should I proceed?', 'Let me know if...' are FORBIDDEN turn-endings.",
90768
+ "- The user is not watching this conversation. Questions written as prose are ignored.",
90769
+ "- If you genuinely cannot proceed, call fn_task_done with a clear explanation \u2014 never write the blocker as plain prose.",
90770
+ "",
90771
+ "What you must do now:",
90772
+ "1. Review the PROMPT.md steps and identify the next pending step.",
90773
+ "2. Do the work for that step immediately \u2014 call fn_task_update, write code, run tests.",
90774
+ "3. Continue until all steps are done, then call fn_task_done.",
90775
+ "Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.",
90776
+ "",
90777
+ "Original task:",
90778
+ buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
90779
+ ].join("\n");
90780
+ } else {
90781
+ retryPrompt = [
90782
+ "Your previous session ended without calling the fn_task_done tool.",
90783
+ "The task may already be complete \u2014 review the current state of the worktree and either:",
90784
+ "1. If the work is done, call fn_task_done with a summary of what was accomplished.",
90785
+ "2. If there is remaining work, finish it and then call fn_task_done.",
90786
+ "",
90787
+ "Original task:",
90788
+ buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
90789
+ ].join("\n");
90790
+ }
90791
+ stuckDetector?.recordActivity(task.id);
90792
+ await promptWithFallback(retrySession2, retryPrompt);
90793
+ checkSessionError(retrySession2);
90794
+ await accumulateSessionTokenUsage(this.store, task.id, retrySession2, {
90795
+ agentId: task.assignedAgentId ?? void 0,
90796
+ role: "executor"
90797
+ });
90798
+ } catch (retryError) {
90799
+ const retryErrorText = retryError instanceof Error ? retryError.message : String(retryError);
90800
+ if (!isMissingWorktreeSessionStartFailure(retryErrorText)) {
90801
+ throw retryError;
90802
+ }
90803
+ const recoveredPath = extractMissingWorktreePathFromSessionStartFailure(retryErrorText) ?? worktreePath;
90804
+ const reclaimMessage = `${task.id}: no-fn_task_done retry hit missing worktree session-start failure (${recoveredPath}) \u2014 clearing stale metadata and requeueing`;
90805
+ executorLog.log(reclaimMessage);
90806
+ await this.store.logEntry(task.id, reclaimMessage, void 0, this.currentRunContext);
90807
+ await this.store.updateTask(task.id, {
90808
+ sessionFile: null,
90809
+ worktree: null,
90810
+ branch: null,
90811
+ baseCommitSha: null
90812
+ });
90813
+ this.activeSessions.delete(task.id);
90814
+ this.tokenUsageBaselines.delete(task.id);
90815
+ retrySession2?.dispose();
90816
+ retryAbortedDueToReclaim = true;
90817
+ break;
90506
90818
  }
90507
- session = retrySession2;
90508
- sessionRef.current = retrySession2;
90509
- this.activeSessions.set(task.id, {
90510
- session: retrySession2,
90511
- seenSteeringIds,
90512
- lastResolvedModelProvider: executorProvider,
90513
- lastResolvedModelId: executorModelId,
90514
- lastTaskModelProvider: detail.modelProvider,
90515
- lastTaskModelId: detail.modelId,
90516
- lastAssignedAgentId: detail.assignedAgentId ?? null
90517
- });
90518
- stuckDetector?.trackTask(task.id, retrySession2);
90519
- let retryPrompt;
90520
- if (pseudoPause.kind !== "none") {
90521
- const shortMatch = (pseudoPause.matched ?? "").slice(0, 120);
90522
- retryPrompt = [
90523
- `Your previous turn ended with a pseudo-pause: "${shortMatch}". This is forbidden.`,
90524
- "",
90525
- "Turn-ending rules you violated:",
90526
- "- You MUST NOT end a turn by asking the user a question, summarizing progress, or requesting permission to continue.",
90527
- "- Phrases like 'If you want, I can continue', 'Should I proceed?', 'Let me know if...' are FORBIDDEN turn-endings.",
90528
- "- The user is not watching this conversation. Questions written as prose are ignored.",
90529
- "- If you genuinely cannot proceed, call fn_task_done with a clear explanation \u2014 never write the blocker as plain prose.",
90530
- "",
90531
- "What you must do now:",
90532
- "1. Review the PROMPT.md steps and identify the next pending step.",
90533
- "2. Do the work for that step immediately \u2014 call fn_task_update, write code, run tests.",
90534
- "3. Continue until all steps are done, then call fn_task_done.",
90535
- "Do NOT ask for permission. Do NOT write a summary. Just call a tool and keep working.",
90536
- "",
90537
- "Original task:",
90538
- buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
90539
- ].join("\n");
90540
- } else {
90541
- retryPrompt = [
90542
- "Your previous session ended without calling the fn_task_done tool.",
90543
- "The task may already be complete \u2014 review the current state of the worktree and either:",
90544
- "1. If the work is done, call fn_task_done with a summary of what was accomplished.",
90545
- "2. If there is remaining work, finish it and then call fn_task_done.",
90546
- "",
90547
- "Original task:",
90548
- buildExecutionPrompt(detail, this.rootDir, settings, worktreePath, this.options.pluginRunner)
90549
- ].join("\n");
90550
- }
90551
- stuckDetector?.recordActivity(task.id);
90552
- await promptWithFallback(retrySession2, retryPrompt);
90553
- checkSessionError(retrySession2);
90554
- await accumulateSessionTokenUsage(this.store, task.id, retrySession2, {
90555
- agentId: task.assignedAgentId ?? void 0,
90556
- role: "executor"
90557
- });
90558
90819
  if (!taskDone) {
90559
90820
  const implicitCheck = await this.store.getTask(task.id);
90560
90821
  if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
@@ -90617,7 +90878,7 @@ ${summary}`,
90617
90878
  } else {
90618
90879
  const priorRequeues = task.taskDoneRetryCount ?? 0;
90619
90880
  const nextRequeueCount = priorRequeues + 1;
90620
- const errorMessage = `Agent finished without calling fn_task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
90881
+ 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)`;
90621
90882
  if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
90622
90883
  await this.store.updateTask(task.id, {
90623
90884
  status: "failed",
@@ -102943,7 +103204,7 @@ function extractJson(raw) {
102943
103204
  if (first >= 0 && last > first) return trimmed.slice(first, last + 1);
102944
103205
  return trimmed;
102945
103206
  }
102946
- var log14, HybridEvaluatorService;
103207
+ var log15, HybridEvaluatorService;
102947
103208
  var init_evaluator = __esm({
102948
103209
  "../engine/src/evaluator.ts"() {
102949
103210
  "use strict";
@@ -102952,7 +103213,7 @@ var init_evaluator = __esm({
102952
103213
  init_eval_followups();
102953
103214
  init_pi();
102954
103215
  init_logger2();
102955
- log14 = createLogger2("evaluator");
103216
+ log15 = createLogger2("evaluator");
102956
103217
  HybridEvaluatorService = class {
102957
103218
  constructor(deps) {
102958
103219
  this.deps = deps;
@@ -103044,7 +103305,7 @@ var init_evaluator = __esm({
103044
103305
  try {
103045
103306
  session.dispose();
103046
103307
  } catch (error) {
103047
- log14.warn(`Evaluator session disposal failed: ${error instanceof Error ? error.message : String(error)}`);
103308
+ log15.warn(`Evaluator session disposal failed: ${error instanceof Error ? error.message : String(error)}`);
103048
103309
  }
103049
103310
  }
103050
103311
  }
@@ -103176,7 +103437,7 @@ function truncateOutput2(stdout, stderr) {
103176
103437
  }
103177
103438
  return combined;
103178
103439
  }
103179
- 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;
103440
+ 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;
103180
103441
  var init_cron_runner = __esm({
103181
103442
  "../engine/src/cron-runner.ts"() {
103182
103443
  "use strict";
@@ -103185,7 +103446,7 @@ var init_cron_runner = __esm({
103185
103446
  init_shell_utils();
103186
103447
  init_pi();
103187
103448
  init_evaluator();
103188
- log15 = createLogger2("cron-runner");
103449
+ log16 = createLogger2("cron-runner");
103189
103450
  FUSION_BINARY_TOKENS = /* @__PURE__ */ new Set([
103190
103451
  "fn",
103191
103452
  "fusion",
@@ -103226,7 +103487,7 @@ var init_cron_runner = __esm({
103226
103487
  start() {
103227
103488
  if (this.running) return;
103228
103489
  this.running = true;
103229
- log15.log(`Started (poll every ${this.pollIntervalMs / 1e3}s, scope: ${this.scope})`);
103490
+ log16.log(`Started (poll every ${this.pollIntervalMs / 1e3}s, scope: ${this.scope})`);
103230
103491
  void this.tick();
103231
103492
  this.pollInterval = setInterval(() => {
103232
103493
  void this.tick();
@@ -103240,7 +103501,7 @@ var init_cron_runner = __esm({
103240
103501
  clearInterval(this.pollInterval);
103241
103502
  this.pollInterval = null;
103242
103503
  }
103243
- log15.log("Stopped");
103504
+ log16.log("Stopped");
103244
103505
  }
103245
103506
  /**
103246
103507
  * Single poll cycle: find due schedules and execute them.
@@ -103264,29 +103525,29 @@ var init_cron_runner = __esm({
103264
103525
  const executedIds = /* @__PURE__ */ new Set();
103265
103526
  for (const schedule of dueSchedules) {
103266
103527
  if (this.inFlight.has(schedule.id)) {
103267
- log15.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 still running from previous tick`);
103528
+ log16.warn(`Skipping ${schedule.name} (${schedule.id}) \u2014 still running from previous tick`);
103268
103529
  continue;
103269
103530
  }
103270
103531
  if (executedIds.has(schedule.id)) {
103271
- log15.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 already executed from another scope this tick`);
103532
+ log16.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 already executed from another scope this tick`);
103272
103533
  continue;
103273
103534
  }
103274
103535
  executedIds.add(schedule.id);
103275
103536
  const scheduleScope = schedule.scope ?? "project";
103276
103537
  if (scheduleScope !== this.scope && this.scope !== "all") {
103277
- log15.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
103538
+ log16.log(`Skipping ${schedule.name} (${schedule.id}) \u2014 belongs to ${scheduleScope} scope, not polling`);
103278
103539
  continue;
103279
103540
  }
103280
- log15.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
103541
+ log16.log(`Executing ${schedule.name} (${schedule.id}) [scope: ${scheduleScope}]`);
103281
103542
  const currentSettings = await this.store.getSettings();
103282
103543
  if (currentSettings.globalPause || currentSettings.enginePaused) {
103283
- log15.log("Pause detected mid-tick \u2014 stopping schedule execution");
103544
+ log16.log("Pause detected mid-tick \u2014 stopping schedule execution");
103284
103545
  break;
103285
103546
  }
103286
103547
  await this.executeSchedule(schedule);
103287
103548
  }
103288
103549
  } catch (err) {
103289
- log15.error(`Tick error: ${err.message}`);
103550
+ log16.error(`Tick error: ${err.message}`);
103290
103551
  } finally {
103291
103552
  this.ticking = false;
103292
103553
  }
@@ -103316,13 +103577,13 @@ var init_cron_runner = __esm({
103316
103577
  try {
103317
103578
  await this.automationStore.recordRun(schedule.id, result);
103318
103579
  } catch (recordErr) {
103319
- log15.error(`Failed to record run for ${schedule.id}: ${recordErr.message}`);
103580
+ log16.error(`Failed to record run for ${schedule.id}: ${recordErr.message}`);
103320
103581
  }
103321
103582
  if (this.onScheduleRunProcessed) {
103322
103583
  try {
103323
103584
  await this.onScheduleRunProcessed(schedule, result);
103324
103585
  } catch (callbackErr) {
103325
- log15.error(
103586
+ log16.error(
103326
103587
  `Post-run callback failed for ${schedule.name} (${schedule.id}): ${callbackErr.message}`
103327
103588
  );
103328
103589
  }
@@ -103333,7 +103594,7 @@ var init_cron_runner = __esm({
103333
103594
  * Execute a legacy single-command schedule.
103334
103595
  */
103335
103596
  async executeLegacyCommand(schedule, startedAt) {
103336
- log15.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
103597
+ log16.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
103337
103598
  if (isInProcessBackupCommand(schedule.command)) {
103338
103599
  return this.executeBackupInProcess(schedule, startedAt);
103339
103600
  }
@@ -103351,7 +103612,7 @@ var init_cron_runner = __esm({
103351
103612
  shell: defaultShell
103352
103613
  });
103353
103614
  const output = truncateOutput2(stdout, stderr);
103354
- log15.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
103615
+ log16.log(`\u2713 ${schedule.name} completed (${output.length} bytes output)`);
103355
103616
  return {
103356
103617
  success: true,
103357
103618
  output,
@@ -103363,7 +103624,7 @@ var init_cron_runner = __esm({
103363
103624
  const stderr = err.stderr ?? "";
103364
103625
  const output = truncateOutput2(stdout, stderr);
103365
103626
  const errorMessage = err.killed ? `Command timed out after ${(schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS8) / 1e3}s` : err.message ?? String(err);
103366
- log15.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
103627
+ log16.warn(`\u2717 ${schedule.name} failed: ${errorMessage}`);
103367
103628
  return {
103368
103629
  success: false,
103369
103630
  output,
@@ -103381,9 +103642,9 @@ var init_cron_runner = __esm({
103381
103642
  async executeBackupInProcess(schedule, startedAt) {
103382
103643
  const action = await this.runBackupActionInProcess();
103383
103644
  if (action.success) {
103384
- log15.log(`\u2713 ${schedule.name} completed in-process`);
103645
+ log16.log(`\u2713 ${schedule.name} completed in-process`);
103385
103646
  } else {
103386
- log15.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
103647
+ log16.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
103387
103648
  }
103388
103649
  return {
103389
103650
  success: action.success,
@@ -103396,9 +103657,9 @@ var init_cron_runner = __esm({
103396
103657
  async executeMemoryBackupInProcess(schedule, startedAt) {
103397
103658
  const action = await this.runMemoryBackupActionInProcess();
103398
103659
  if (action.success) {
103399
- log15.log(`\u2713 ${schedule.name} completed in-process`);
103660
+ log16.log(`\u2713 ${schedule.name} completed in-process`);
103400
103661
  } else {
103401
- log15.warn(`\u2717 ${schedule.name} in-process memory backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
103662
+ log16.warn(`\u2717 ${schedule.name} in-process memory backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
103402
103663
  }
103403
103664
  return {
103404
103665
  success: action.success,
@@ -103487,25 +103748,25 @@ var init_cron_runner = __esm({
103487
103748
  */
103488
103749
  async executeSteps(schedule, startedAt) {
103489
103750
  const steps = schedule.steps;
103490
- log15.log(`Executing ${schedule.name} (${schedule.id}): ${steps.length} steps`);
103751
+ log16.log(`Executing ${schedule.name} (${schedule.id}): ${steps.length} steps`);
103491
103752
  const stepResults = [];
103492
103753
  let overallSuccess = true;
103493
103754
  let stoppedEarly = false;
103494
103755
  for (let i = 0; i < steps.length; i++) {
103495
103756
  const step = steps[i];
103496
- log15.log(` Step ${i + 1}/${steps.length}: ${step.name} (${step.type})`);
103757
+ log16.log(` Step ${i + 1}/${steps.length}: ${step.name} (${step.type})`);
103497
103758
  const stepResult = await this.executeStep(schedule, step, i);
103498
103759
  stepResults.push(stepResult);
103499
103760
  if (!stepResult.success) {
103500
103761
  overallSuccess = false;
103501
103762
  if (!step.continueOnFailure) {
103502
- log15.warn(` Step "${step.name}" failed \u2014 stopping execution`);
103763
+ log16.warn(` Step "${step.name}" failed \u2014 stopping execution`);
103503
103764
  stoppedEarly = true;
103504
103765
  break;
103505
103766
  }
103506
- log15.warn(` Step "${step.name}" failed \u2014 continuing (continueOnFailure=true)`);
103767
+ log16.warn(` Step "${step.name}" failed \u2014 continuing (continueOnFailure=true)`);
103507
103768
  } else {
103508
- log15.log(` \u2713 Step "${step.name}" completed`);
103769
+ log16.log(` \u2713 Step "${step.name}" completed`);
103509
103770
  }
103510
103771
  }
103511
103772
  const outputParts = [];
@@ -103518,7 +103779,7 @@ var init_cron_runner = __esm({
103518
103779
  const failedSteps = stepResults.filter((sr) => !sr.success);
103519
103780
  const error = failedSteps.length > 0 ? `${failedSteps.length} step(s) failed: ${failedSteps.map((s) => s.stepName).join(", ")}${stoppedEarly ? " (execution stopped)" : ""}` : void 0;
103520
103781
  const status = overallSuccess ? "\u2713" : "\u2717";
103521
- log15.log(`${status} ${schedule.name}: ${stepResults.length}/${steps.length} steps executed, ${failedSteps.length} failed`);
103782
+ log16.log(`${status} ${schedule.name}: ${stepResults.length}/${steps.length} steps executed, ${failedSteps.length} failed`);
103522
103783
  return {
103523
103784
  success: overallSuccess,
103524
103785
  output,
@@ -103661,8 +103922,8 @@ var init_cron_runner = __esm({
103661
103922
  const modelProvider = step.modelProvider?.trim() || defaultModel.provider;
103662
103923
  const modelId = step.modelId?.trim() || defaultModel.modelId;
103663
103924
  const model = modelProvider && modelId ? `${modelProvider}/${modelId}` : "default";
103664
- log15.log(` AI prompt step "${step.name}" using model: ${model}`);
103665
- log15.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
103925
+ log16.log(` AI prompt step "${step.name}" using model: ${model}`);
103926
+ log16.log(` Prompt: ${step.prompt.slice(0, 100)}${step.prompt.length > 100 ? "\u2026" : ""}`);
103666
103927
  try {
103667
103928
  const resultPromise = this.aiPromptExecutor(step.prompt, modelProvider, modelId);
103668
103929
  const timeoutPromise = new Promise((_resolve, reject2) => {
@@ -103671,7 +103932,7 @@ var init_cron_runner = __esm({
103671
103932
  const response = await Promise.race([resultPromise, timeoutPromise]);
103672
103933
  const responseText = String(response ?? "");
103673
103934
  const output = responseText.length > MAX_OUTPUT_LENGTH ? responseText.slice(0, MAX_OUTPUT_LENGTH) + "\n[output truncated]" : responseText;
103674
- log15.log(` \u2713 AI prompt step "${step.name}" completed (${responseText.length} chars)`);
103935
+ log16.log(` \u2713 AI prompt step "${step.name}" completed (${responseText.length} chars)`);
103675
103936
  return {
103676
103937
  stepId: step.id,
103677
103938
  stepName: step.name,
@@ -103683,7 +103944,7 @@ var init_cron_runner = __esm({
103683
103944
  };
103684
103945
  } catch (err) {
103685
103946
  const errorMessage = err.message ?? String(err);
103686
- log15.warn(` \u2717 AI prompt step "${step.name}" failed: ${errorMessage}`);
103947
+ log16.warn(` \u2717 AI prompt step "${step.name}" failed: ${errorMessage}`);
103687
103948
  return {
103688
103949
  stepId: step.id,
103689
103950
  stepName: step.name,
@@ -103727,7 +103988,7 @@ var init_cron_runner = __esm({
103727
103988
  try {
103728
103989
  const task = await this.store.createTask(taskInput);
103729
103990
  const output = `Created task ${task.id}: ${task.title || task.description.slice(0, 80)}`;
103730
- log15.log(` \u2713 Create-task step "${step.name}" created task ${task.id}`);
103991
+ log16.log(` \u2713 Create-task step "${step.name}" created task ${task.id}`);
103731
103992
  return {
103732
103993
  stepId: step.id,
103733
103994
  stepName: step.name,
@@ -103739,7 +104000,7 @@ var init_cron_runner = __esm({
103739
104000
  };
103740
104001
  } catch (err) {
103741
104002
  const errorMessage = err.message ?? String(err);
103742
- log15.warn(` \u2717 Create-task step "${step.name}" failed: ${errorMessage}`);
104003
+ log16.warn(` \u2717 Create-task step "${step.name}" failed: ${errorMessage}`);
103743
104004
  return {
103744
104005
  stepId: step.id,
103745
104006
  stepName: step.name,
@@ -103781,7 +104042,7 @@ function truncateOutput3(stdout, stderr) {
103781
104042
  }
103782
104043
  return output;
103783
104044
  }
103784
- var import_cron_parser4, log16, execAsync12, DEFAULT_TIMEOUT_MS9, MAX_BUFFER2, MAX_OUTPUT_LENGTH2, MAX_CATCH_UP_INTERVALS, RoutineRunner;
104045
+ var import_cron_parser4, log17, execAsync12, DEFAULT_TIMEOUT_MS9, MAX_BUFFER2, MAX_OUTPUT_LENGTH2, MAX_CATCH_UP_INTERVALS, RoutineRunner;
103785
104046
  var init_routine_runner = __esm({
103786
104047
  "../engine/src/routine-runner.ts"() {
103787
104048
  "use strict";
@@ -103789,7 +104050,7 @@ var init_routine_runner = __esm({
103789
104050
  init_cron_runner();
103790
104051
  init_logger2();
103791
104052
  init_shell_utils();
103792
- log16 = createLogger2("routine-runner");
104053
+ log17 = createLogger2("routine-runner");
103793
104054
  execAsync12 = promisify17(exec14);
103794
104055
  DEFAULT_TIMEOUT_MS9 = 5 * 60 * 1e3;
103795
104056
  MAX_BUFFER2 = 1024 * 1024;
@@ -103826,7 +104087,7 @@ var init_routine_runner = __esm({
103826
104087
  }
103827
104088
  const concurrency = routine.executionPolicy ?? "queue";
103828
104089
  if (concurrency === "reject" && this.inFlightExecutions.has(routineId)) {
103829
- log16.log(`Routine ${routineId} rejected \u2014 already running`);
104090
+ log17.log(`Routine ${routineId} rejected \u2014 already running`);
103830
104091
  return {
103831
104092
  routineId,
103832
104093
  success: false,
@@ -103837,7 +104098,7 @@ var init_routine_runner = __esm({
103837
104098
  };
103838
104099
  }
103839
104100
  if (concurrency === "queue" && this.inFlightExecutions.has(routineId)) {
103840
- log16.log(`Routine ${routineId} queued \u2014 waiting for existing execution`);
104101
+ log17.log(`Routine ${routineId} queued \u2014 waiting for existing execution`);
103841
104102
  const existingResult = await this.inFlightExecutions.get(routineId);
103842
104103
  if (existingResult) {
103843
104104
  await existingResult;
@@ -103885,7 +104146,7 @@ var init_routine_runner = __esm({
103885
104146
  };
103886
104147
  } catch (err) {
103887
104148
  const errorMessage = err instanceof Error ? err.message : String(err);
103888
- log16.error(`Routine ${routineId} execution failed: ${errorMessage}`);
104149
+ log17.error(`Routine ${routineId} execution failed: ${errorMessage}`);
103889
104150
  try {
103890
104151
  await this.options.routineStore.completeRoutineExecution(routineId, {
103891
104152
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -103893,7 +104154,7 @@ var init_routine_runner = __esm({
103893
104154
  error: errorMessage
103894
104155
  });
103895
104156
  } catch (persistError) {
103896
- log16.error(`[${routineId}] Failed to persist error state: ${persistError}`);
104157
+ log17.error(`[${routineId}] Failed to persist error state: ${persistError}`);
103897
104158
  }
103898
104159
  return {
103899
104160
  routineId,
@@ -104155,7 +104416,7 @@ var init_routine_runner = __esm({
104155
104416
  if (missedIntervals.length === 0) {
104156
104417
  return;
104157
104418
  }
104158
- log16.log(`[${routine.id}] Running ${missedIntervals.length} catch-up executions`);
104419
+ log17.log(`[${routine.id}] Running ${missedIntervals.length} catch-up executions`);
104159
104420
  for (const missedInterval of missedIntervals) {
104160
104421
  try {
104161
104422
  await this.executeRoutine(routine.id, "cron", {
@@ -104163,11 +104424,11 @@ var init_routine_runner = __esm({
104163
104424
  missedInterval: missedInterval.toISOString()
104164
104425
  });
104165
104426
  } catch (err) {
104166
- log16.error(`[${routine.id}] Catch-up execution failed: ${err}`);
104427
+ log17.error(`[${routine.id}] Catch-up execution failed: ${err}`);
104167
104428
  }
104168
104429
  }
104169
104430
  } catch (err) {
104170
- log16.error(`[${routine.id}] Error calculating catch-up intervals: ${err}`);
104431
+ log17.error(`[${routine.id}] Error calculating catch-up intervals: ${err}`);
104171
104432
  }
104172
104433
  }
104173
104434
  /**
@@ -104753,91 +105014,6 @@ var init_stuck_task_detector = __esm({
104753
105014
  }
104754
105015
  });
104755
105016
 
104756
- // ../engine/src/restart-recovery-coordinator.ts
104757
- function hasStepProgress(task) {
104758
- const steps = Array.isArray(task.steps) ? task.steps : [];
104759
- return steps.some((step) => step.status === "done" || step.status === "in-progress" || step.status === "skipped");
104760
- }
104761
- function isNoTaskDoneFailure(task) {
104762
- return task.status === "failed" && typeof task.error === "string" && task.error.toLowerCase().includes("without calling fn_task_done");
104763
- }
104764
- function findMissingWorktreeSessionPrefix(error) {
104765
- for (const prefix of MISSING_WORKTREE_SESSION_PREFIXES) {
104766
- if (error.includes(prefix)) {
104767
- return prefix;
104768
- }
104769
- }
104770
- return null;
104771
- }
104772
- function isMissingWorktreeSessionStartFailure(error) {
104773
- if (typeof error !== "string") {
104774
- return false;
104775
- }
104776
- return findMissingWorktreeSessionPrefix(error) !== null;
104777
- }
104778
- function extractMissingWorktreePathFromSessionStartFailure(error) {
104779
- if (typeof error !== "string") return null;
104780
- const prefix = findMissingWorktreeSessionPrefix(error);
104781
- if (!prefix) return null;
104782
- const idx = error.indexOf(prefix);
104783
- const pathPart = error.slice(idx + prefix.length).trim();
104784
- return pathPart.length > 0 ? pathPart : null;
104785
- }
104786
- function isRecoverableMissingWorktreeReviewFailure(task) {
104787
- return task.column === "in-review" && !task.paused && task.status === "failed" && isMissingWorktreeSessionStartFailure(task.error) && hasStepProgress(task);
104788
- }
104789
- var log17, MISSING_WORKTREE_SESSION_PREFIXES, RestartRecoveryCoordinator;
104790
- var init_restart_recovery_coordinator = __esm({
104791
- "../engine/src/restart-recovery-coordinator.ts"() {
104792
- "use strict";
104793
- init_logger2();
104794
- log17 = createLogger2("restart-recovery");
104795
- MISSING_WORKTREE_SESSION_PREFIXES = [
104796
- "Refusing to start coding agent in missing worktree:",
104797
- "Refusing to start coding agent in incomplete worktree:",
104798
- "Refusing to start coding agent in unregistered git worktree:"
104799
- ];
104800
- RestartRecoveryCoordinator = class {
104801
- constructor(store, executor) {
104802
- this.store = store;
104803
- this.executor = executor;
104804
- }
104805
- async recoverInterruptedRuns() {
104806
- const allInProgress = await this.store.listTasks({ slim: true, column: "in-progress" });
104807
- const candidates = allInProgress.filter((task) => task.column === "in-progress" && !task.paused);
104808
- if (candidates.length === 0) return;
104809
- let requeued = 0;
104810
- for (const task of candidates) {
104811
- if (!this.mustSafeRetry(task)) continue;
104812
- await this.safeRequeue(task);
104813
- requeued++;
104814
- }
104815
- if (requeued > 0) {
104816
- log17.log(`Restart recovery requeued ${requeued} interrupted task(s) for safe retry`);
104817
- }
104818
- await this.executor.resumeOrphaned();
104819
- }
104820
- mustSafeRetry(task) {
104821
- return isNoTaskDoneFailure(task) && !hasStepProgress(task);
104822
- }
104823
- async safeRequeue(task) {
104824
- await this.store.updateTask(task.id, {
104825
- status: "stuck-killed",
104826
- worktree: null,
104827
- branch: null,
104828
- sessionFile: null,
104829
- error: null
104830
- });
104831
- await this.store.logEntry(
104832
- task.id,
104833
- "Restart recovery: interrupted run had no step progress and no fn_task_done \u2014 requeued to todo for safe retry"
104834
- );
104835
- await this.store.moveTask(task.id, "todo");
104836
- }
104837
- };
104838
- }
104839
- });
104840
-
104841
105017
  // ../engine/src/self-healing.ts
104842
105018
  import { exec as exec15, execSync as execSync4 } from "node:child_process";
104843
105019
  import { promisify as promisify18 } from "node:util";
@@ -110615,6 +110791,13 @@ var init_in_process_runtime = __esm({
110615
110791
  getMessageStore() {
110616
110792
  return this.messageStore;
110617
110793
  }
110794
+ /**
110795
+ * Get the ChatStore instance (if initialized).
110796
+ * Returns undefined before start() or if initialization fails.
110797
+ */
110798
+ getChatStore() {
110799
+ return this.chatStore;
110800
+ }
110618
110801
  /**
110619
110802
  * Get the project's Scheduler instance.
110620
110803
  * @throws Error if runtime has not been started
@@ -113588,6 +113771,10 @@ ${detail}`
113588
113771
  getMessageStore() {
113589
113772
  return this.runtime.getMessageStore();
113590
113773
  }
113774
+ /** Get the ChatStore (if initialized). Returns undefined before start(). */
113775
+ getChatStore() {
113776
+ return this.runtime.getChatStore();
113777
+ }
113591
113778
  attachChatStore(chatStore) {
113592
113779
  this.notificationService?.attachChatStore(chatStore);
113593
113780
  }
@@ -119389,6 +119576,7 @@ PREFER structured question types over free-text. This makes the interview faster
119389
119576
  - Features should be specific and actionable
119390
119577
  - ALWAYS include verification/acceptance criteria at every level:
119391
119578
  - Milestone: "verification" field \u2014 how to confirm this phase is complete (e.g., "All API endpoints return correct responses, integration tests pass")
119579
+ - Milestone: optional "acceptanceCriteria" field for explicit milestone-level completion bars (if omitted, acceptance can be auto-derived from child feature criteria/descriptions)
119392
119580
  - Slice: "verification" field \u2014 how to confirm this work unit is done (e.g., "Auth flow works end-to-end from signup through login")
119393
119581
  - Feature: "acceptanceCriteria" field \u2014 how to verify this specific deliverable (e.g., "JWT tokens expire after 1 hour and refresh correctly")
119394
119582
  - Suggest sensible defaults and push for specificity
@@ -119411,7 +119599,7 @@ For text question (use ONLY for names, URLs, or truly unique free-form input):
119411
119599
  {"type": "question", "data": {"id": "q-project-name", "type": "text", "question": "What is the project or product name?", "description": "Used in documentation and configuration"}}
119412
119600
 
119413
119601
  For completion (when you have enough information):
119414
- {"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"}]}]}]}}`;
119602
+ {"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"}]}]}]}}`;
119415
119603
  sessions3 = /* @__PURE__ */ new Map();
119416
119604
  rateLimits3 = /* @__PURE__ */ new Map();
119417
119605
  cleanupInterval4 = setInterval(cleanupExpiredSessions3, CLEANUP_INTERVAL_MS4);
@@ -130592,9 +130780,13 @@ function cacheKeyForStore(store) {
130592
130780
  }
130593
130781
  function getOrCreateScopedChatStore(store, fallbackChatStore) {
130594
130782
  const key = cacheKeyForStore(store);
130783
+ if (fallbackChatStore) {
130784
+ scopedChatStoreCache.set(key, fallbackChatStore);
130785
+ return fallbackChatStore;
130786
+ }
130595
130787
  const cached = scopedChatStoreCache.get(key);
130596
130788
  if (cached) return cached;
130597
- const chatStore = fallbackChatStore ?? new ChatStore(store.getFusionDir(), store.getDatabase());
130789
+ const chatStore = new ChatStore(store.getFusionDir(), store.getDatabase());
130598
130790
  scopedChatStoreCache.set(key, chatStore);
130599
130791
  return chatStore;
130600
130792
  }
@@ -130609,9 +130801,10 @@ async function resolveProjectChatContext(options) {
130609
130801
  const engine = engineManager?.getEngine(projectId);
130610
130802
  try {
130611
130803
  const scopedStore = engine?.getTaskStore() ?? await getOrCreateProjectStore(projectId);
130804
+ const engineChatStore = engine?.getChatStore?.();
130612
130805
  return {
130613
130806
  store: scopedStore,
130614
- chatStore: getOrCreateScopedChatStore(scopedStore)
130807
+ chatStore: getOrCreateScopedChatStore(scopedStore, engineChatStore)
130615
130808
  };
130616
130809
  } catch {
130617
130810
  return {
@@ -130844,14 +131037,17 @@ function registerChatRoomRoutes(ctx) {
130844
131037
  const { chatStore } = await resolveRoomScopedServices(req, getRequestedProjectId(req));
130845
131038
  const room = chatStore.getRoom(roomId);
130846
131039
  if (!room) throw notFound(`Chat room ${roomId} not found`);
130847
- const { limit: limitStr, offset: offsetStr, before } = req.query;
131040
+ const { limit: limitStr, offset: offsetStr, before, order: orderStr } = req.query;
130848
131041
  const limit = limitStr !== void 0 ? parseInt(String(limitStr), 10) : 50;
130849
131042
  const offset = offsetStr !== void 0 ? parseInt(String(offsetStr), 10) : 0;
131043
+ const order = orderStr === void 0 ? "asc" : String(orderStr);
130850
131044
  if (!Number.isFinite(limit) || limit < 1) throw badRequest("limit must be a positive integer");
130851
131045
  if (!Number.isFinite(offset) || offset < 0) throw badRequest("offset must be a non-negative integer");
131046
+ if (order !== "asc" && order !== "desc") throw badRequest("order must be either asc or desc");
130852
131047
  const messages = chatStore.getRoomMessages(roomId, {
130853
131048
  limit,
130854
131049
  offset,
131050
+ order,
130855
131051
  ...before ? { before } : {}
130856
131052
  });
130857
131053
  res.json({ messages });
@@ -181307,7 +181503,7 @@ var init_register_auth_routes = __esm({
181307
181503
  if (!origin || isLocalhostOrigin(origin)) {
181308
181504
  return false;
181309
181505
  }
181310
- if (providerId === "openai-codex" || providerId === "anthropic") {
181506
+ if (providerId === "openai-codex" || providerId === "anthropic" || providerId === "github-copilot") {
181311
181507
  return false;
181312
181508
  }
181313
181509
  return true;
@@ -183080,7 +183276,8 @@ function createMissionRouter(store, missionAutopilot, aiSessionStore, missionExe
183080
183276
  const milestone = missionStore.addMilestone(mission.id, {
183081
183277
  title: milestoneData.title,
183082
183278
  description: milestoneData.description || void 0,
183083
- verification: milestoneData.verification
183279
+ verification: milestoneData.verification,
183280
+ acceptanceCriteria: milestoneData.acceptanceCriteria
183084
183281
  });
183085
183282
  const milestoneAssertionText = milestoneData.verification || milestoneData.description || `Verify milestone completion: ${milestoneData.title}`;
183086
183283
  missionStore.addContractAssertion(milestone.id, {
@@ -183115,6 +183312,7 @@ function createMissionRouter(store, missionAutopilot, aiSessionStore, missionExe
183115
183312
  missionStore.linkFeatureToAssertion(feature.id, assertion.id);
183116
183313
  }
183117
183314
  }
183315
+ missionStore.applyDerivedMilestoneAcceptanceCriteria(milestone.id);
183118
183316
  }
183119
183317
  cleanupMissionInterviewSession2(sessionId);
183120
183318
  const result = missionStore.getMissionWithHierarchy(mission.id);
@@ -183321,7 +183519,7 @@ function createMissionRouter(store, missionAutopilot, aiSessionStore, missionExe
183321
183519
  "/:missionId/milestones",
183322
183520
  catchTypedHandler(async (req, res) => {
183323
183521
  const { missionId } = req.params;
183324
- const { title, description, dependencies } = req.body;
183522
+ const { title, description, dependencies, acceptanceCriteria } = req.body;
183325
183523
  if (!validateMissionId(missionId)) {
183326
183524
  throw badRequest("Invalid mission ID format");
183327
183525
  }
@@ -183332,10 +183530,12 @@ function createMissionRouter(store, missionAutopilot, aiSessionStore, missionExe
183332
183530
  const validatedTitle = validateTitle(title);
183333
183531
  const validatedDescription = validateDescription2(description);
183334
183532
  const validatedDependencies = validateStringArray(dependencies, "dependencies");
183533
+ const validatedAcceptanceCriteria = validateDescription2(acceptanceCriteria);
183335
183534
  const input = {
183336
183535
  title: validatedTitle,
183337
183536
  description: validatedDescription,
183338
- dependencies: validatedDependencies
183537
+ dependencies: validatedDependencies,
183538
+ acceptanceCriteria: validatedAcceptanceCriteria
183339
183539
  };
183340
183540
  const milestone = missionStore.addMilestone(missionId, input);
183341
183541
  res.status(201).json(milestone);
@@ -183384,7 +183584,7 @@ function createMissionRouter(store, missionAutopilot, aiSessionStore, missionExe
183384
183584
  "/milestones/:milestoneId",
183385
183585
  catchTypedHandler(async (req, res) => {
183386
183586
  const { milestoneId } = req.params;
183387
- const { title, description, status, dependencies } = req.body;
183587
+ const { title, description, status, dependencies, acceptanceCriteria } = req.body;
183388
183588
  if (!validateMilestoneId(milestoneId)) {
183389
183589
  throw badRequest("Invalid milestone ID format");
183390
183590
  }
@@ -183401,6 +183601,9 @@ function createMissionRouter(store, missionAutopilot, aiSessionStore, missionExe
183401
183601
  if (dependencies !== void 0) {
183402
183602
  updates.dependencies = validateStringArray(dependencies, "dependencies");
183403
183603
  }
183604
+ if (acceptanceCriteria !== void 0) {
183605
+ updates.acceptanceCriteria = validateDescription2(acceptanceCriteria);
183606
+ }
183404
183607
  if (Object.keys(updates).length === 0) {
183405
183608
  throw badRequest("No valid fields to update");
183406
183609
  }
@@ -198903,7 +199106,7 @@ function createServer(store, options) {
198903
199106
  if (engineManager) {
198904
199107
  const engine2 = engineManager.getEngine(projectId);
198905
199108
  scopedStore = engine2?.getTaskStore() ?? await getOrCreateProjectStore(projectId);
198906
- scopedChatStore = getOrCreateScopedChatStore(scopedStore);
199109
+ scopedChatStore = getOrCreateScopedChatStore(scopedStore, engine2?.getChatStore?.());
198907
199110
  agentStore = engine2?.getAgentStore();
198908
199111
  messageStore = engine2?.getMessageStore();
198909
199112
  automationStore = engine2?.getAutomationStore();