@runfusion/fusion 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/bin.js +434 -103
  2. package/dist/client/assets/{AgentDetailView-Ca1euvPo.js → AgentDetailView-khrspqE3.js} +3 -3
  3. package/dist/client/assets/{AgentsView-pile-9ZM.js → AgentsView-DvN5EsoE.js} +11 -11
  4. package/dist/client/assets/AgentsView-MotzGhZJ.css +1 -0
  5. package/dist/client/assets/{ChatView-C4SCF_Hj.js → ChatView-DsETFRQp.js} +1 -1
  6. package/dist/client/assets/{DevServerView-1HvWligZ.js → DevServerView-D1_7SL1h.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-B1eztZna.js → DirectoryPicker-BBcm5G9F.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-D4m9s6Ye.js → DocumentsView-OuVpmTPp.js} +1 -1
  9. package/dist/client/assets/InsightsView-4KiUKzbz.css +1 -0
  10. package/dist/client/assets/InsightsView-DMC_bLUc.js +11 -0
  11. package/dist/client/assets/{MemoryView-DEmn8fA2.js → MemoryView-cI4IK-lz.js} +1 -1
  12. package/dist/client/assets/{NodesView-Blg8NHiu.js → NodesView-C_LWLFNr.js} +2 -2
  13. package/dist/client/assets/{PiExtensionsManager-DkXenPK0.js → PiExtensionsManager-DCTHvy2u.js} +1 -1
  14. package/dist/client/assets/{PluginManager-Czso7ZUF.js → PluginManager-BXMpkZx-.js} +1 -1
  15. package/dist/client/assets/{RoadmapsView-DATopkaE.js → RoadmapsView-BHTePn2u.js} +1 -1
  16. package/dist/client/assets/SettingsModal-2s-L1oWD.js +31 -0
  17. package/dist/client/assets/{SettingsModal-D_mcRJO2.js → SettingsModal-BSZIno8y.js} +1 -1
  18. package/dist/client/assets/{SetupWizardModal-JBNr-XIW.js → SetupWizardModal-DVoRhy_V.js} +1 -1
  19. package/dist/client/assets/{SkillsView-CkT6-elZ.js → SkillsView-CK52SRz5.js} +1 -1
  20. package/dist/client/assets/{TodoView-CstzLvjw.js → TodoView-BKqIV8P6.js} +1 -1
  21. package/dist/client/assets/{folder-open-BEDPztlF.js → folder-open-C0SfzRFt.js} +1 -1
  22. package/dist/client/assets/index-DawWARY5.css +1 -0
  23. package/dist/client/assets/index-DiC9GfBH.js +656 -0
  24. package/dist/client/assets/{list-checks-DgZgg3rh.js → list-checks-D5c428sr.js} +1 -1
  25. package/dist/client/assets/{star-DXieIAlP.js → star-GGmbVi2b.js} +1 -1
  26. package/dist/client/assets/{upload-Cbvs_TSB.js → upload-CrJJybRJ.js} +1 -1
  27. package/dist/client/assets/{users-DAMIrlue.js → users-C56SMdh4.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/extension.js +427 -103
  31. package/dist/pi-claude-cli/package.json +1 -1
  32. package/package.json +1 -1
  33. package/dist/client/assets/AgentsView-Cq-SEhLc.css +0 -1
  34. package/dist/client/assets/InsightsView-6LHF7OdE.css +0 -1
  35. package/dist/client/assets/InsightsView-u535o96R.js +0 -11
  36. package/dist/client/assets/SettingsModal-Cs5qO84M.js +0 -31
  37. package/dist/client/assets/index-CPStj9Az.css +0 -1
  38. package/dist/client/assets/index-DZB5f-Bl.js +0 -656
package/dist/extension.js CHANGED
@@ -1342,7 +1342,7 @@ function getAvailableTemplates(config) {
1342
1342
  function getTemplatesForRole(role, config) {
1343
1343
  return getAvailableTemplates(config).filter((t) => t.role === role);
1344
1344
  }
1345
- var EXECUTOR_PROMPT_TEXT, TRIAGE_PROMPT_TEXT, REVIEWER_PROMPT_TEXT, MERGER_BASE_PROMPT_TEXT, SENIOR_ENGINEER_PROMPT_TEXT, STRICT_REVIEWER_PROMPT_TEXT, CONCISE_TRIAGE_PROMPT_TEXT, BUILTIN_AGENT_PROMPTS;
1345
+ var EXECUTOR_PROMPT_TEXT, TRIAGE_PROMPT_TEXT, REVIEWER_PROMPT_TEXT, MERGER_BASE_PROMPT_TEXT, SENIOR_ENGINEER_PROMPT_TEXT, STRICT_REVIEWER_PROMPT_TEXT, CONCISE_TRIAGE_PROMPT_TEXT, EXECUTOR_HEARTBEAT_GUIDANCE, TRIAGE_HEARTBEAT_GUIDANCE, REVIEWER_HEARTBEAT_GUIDANCE, MERGER_HEARTBEAT_GUIDANCE, SENIOR_ENGINEER_HEARTBEAT_GUIDANCE, STRICT_REVIEWER_HEARTBEAT_GUIDANCE, CONCISE_TRIAGE_HEARTBEAT_GUIDANCE, BUILTIN_AGENT_PROMPTS;
1346
1346
  var init_agent_prompts = __esm({
1347
1347
  "../core/src/agent-prompts.ts"() {
1348
1348
  "use strict";
@@ -2164,13 +2164,64 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2164
2164
  5. **No placeholders:** Real content only.
2165
2165
  6. **Read first:** Examine codebase before writing spec.
2166
2166
  7. **Be concise:** Short descriptions, minimal prose. Focus on what matters.`;
2167
+ EXECUTOR_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2168
+
2169
+ Treat each heartbeat as a short autonomous execution cycle.
2170
+
2171
+ - If a task is assigned: inspect the latest task state, continue the next concrete implementation step, run the smallest useful verification, and either advance the task or log the blocker precisely.
2172
+ - If no task is assigned: execute your standing instructions. Review unread messages, scan for blocked or failing engineering work, create narrowly scoped follow-up tasks, and capture durable implementation notes other agents will need later.
2173
+ - Do not idle simply because no task is linked. Use heartbeat time to reduce engineering risk, unblock work, and keep execution moving in small, concrete increments.`;
2174
+ TRIAGE_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2175
+
2176
+ Use heartbeat runs to keep the planning pipeline healthy.
2177
+
2178
+ - If a task is assigned: turn the rough request into a complete, execution-ready PROMPT.md with clear scope, steps, dependencies, and verification criteria.
2179
+ - If no task is assigned: execute your planning instructions. Patrol for vague requests, blocked tasks that need better specification, review follow-ups that should become new tasks, and dependency gaps that are slowing executors down.
2180
+ - Favor ambiguity reduction over busywork. Every heartbeat should leave the queue more actionable than you found it.`;
2181
+ REVIEWER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2182
+
2183
+ Use heartbeat runs to keep review quality high and queues moving.
2184
+
2185
+ - If a task is assigned: perform the review with findings first, focusing on correctness, regressions, missing tests, and operational risk.
2186
+ - If no task is assigned: execute your review instructions. Look for work waiting on review, failed validations, suspicious recent changes, and places where a second pass would prevent a bad merge.
2187
+ - Prefer surfacing concrete findings, follow-up tasks, or merge blockers over rewriting implementation yourself.`;
2188
+ MERGER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2189
+
2190
+ Use heartbeat runs to keep merge-ready work from stalling.
2191
+
2192
+ - If a task is assigned: verify merge preconditions, resolve the next safe merge step, and surface conflicts or missing gates immediately.
2193
+ - If no task is assigned: execute your merge instructions. Inspect the in-review and merge-ready queue, look for unresolved conflicts, missing approvals, broken post-review state, and tasks that are ready for the final merge push.
2194
+ - Optimize for safe flow, not raw throughput. Clear blockers, communicate risks, and only move merge work forward when the repository stays trustworthy.`;
2195
+ SENIOR_ENGINEER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2196
+
2197
+ Treat each heartbeat as an autonomous senior-engineering pass.
2198
+
2199
+ - If a task is assigned: push the implementation forward decisively, making sound architectural choices, validating risky changes early, and documenting trade-offs that downstream agents should inherit.
2200
+ - If no task is assigned: execute your standing instructions. Hunt for architectural drift, flaky quality gates, latent integration risk, and follow-up work that needs a strong technical owner.
2201
+ - Spend heartbeat time where leverage is highest: unblock teams, reduce complexity, and turn vague engineering risk into concrete next actions.`;
2202
+ STRICT_REVIEWER_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2203
+
2204
+ Use heartbeat runs to enforce a high review bar.
2205
+
2206
+ - If a task is assigned: review for worst-case failure modes first, especially security, backward compatibility, edge cases, and missing regression coverage.
2207
+ - If no task is assigned: execute your review instructions. Look for merges that feel under-reviewed, risky diffs that deserve another pass, and follow-up work needed before code should land.
2208
+ - Bias toward precise findings and explicit risk articulation. A quiet heartbeat should mean the code is genuinely clean, not that you stopped looking.`;
2209
+ CONCISE_TRIAGE_HEARTBEAT_GUIDANCE = `## Heartbeat Run Behavior
2210
+
2211
+ Keep heartbeat output lean and useful.
2212
+
2213
+ - If a task is assigned: produce the minimum complete PROMPT.md needed for an executor to act safely.
2214
+ - If no task is assigned: execute your planning instructions, scan for underspecified or blocked work, and turn it into short, actionable task specs or follow-up tickets.
2215
+ - Prefer crisp decisions, clear file scope, and concrete verification steps over narrative detail.`;
2167
2216
  BUILTIN_AGENT_PROMPTS = [
2168
2217
  {
2169
2218
  id: "default-executor",
2170
2219
  name: "Default Executor",
2171
2220
  description: "Standard task execution agent with full tooling and review support.",
2172
2221
  role: "executor",
2173
- prompt: EXECUTOR_PROMPT_TEXT,
2222
+ prompt: `${EXECUTOR_PROMPT_TEXT}
2223
+
2224
+ ${EXECUTOR_HEARTBEAT_GUIDANCE}`,
2174
2225
  builtIn: true
2175
2226
  },
2176
2227
  {
@@ -2178,7 +2229,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2178
2229
  name: "Default Triage",
2179
2230
  description: "Standard task specification agent producing detailed PROMPT.md files.",
2180
2231
  role: "triage",
2181
- prompt: TRIAGE_PROMPT_TEXT,
2232
+ prompt: `${TRIAGE_PROMPT_TEXT}
2233
+
2234
+ ${TRIAGE_HEARTBEAT_GUIDANCE}`,
2182
2235
  builtIn: true
2183
2236
  },
2184
2237
  {
@@ -2186,7 +2239,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2186
2239
  name: "Default Reviewer",
2187
2240
  description: "Standard independent code and plan reviewer with balanced criteria.",
2188
2241
  role: "reviewer",
2189
- prompt: REVIEWER_PROMPT_TEXT,
2242
+ prompt: `${REVIEWER_PROMPT_TEXT}
2243
+
2244
+ ${REVIEWER_HEARTBEAT_GUIDANCE}`,
2190
2245
  builtIn: true
2191
2246
  },
2192
2247
  {
@@ -2194,7 +2249,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2194
2249
  name: "Default Merger",
2195
2250
  description: "Standard merge agent for squash merges with conflict resolution.",
2196
2251
  role: "merger",
2197
- prompt: MERGER_BASE_PROMPT_TEXT,
2252
+ prompt: `${MERGER_BASE_PROMPT_TEXT}
2253
+
2254
+ ${MERGER_HEARTBEAT_GUIDANCE}`,
2198
2255
  builtIn: true
2199
2256
  },
2200
2257
  {
@@ -2202,7 +2259,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2202
2259
  name: "Senior Engineer",
2203
2260
  description: "Autonomous executor with architectural awareness, performance focus, and minimal hand-holding. Makes independent decisions on routine matters.",
2204
2261
  role: "executor",
2205
- prompt: SENIOR_ENGINEER_PROMPT_TEXT,
2262
+ prompt: `${SENIOR_ENGINEER_PROMPT_TEXT}
2263
+
2264
+ ${SENIOR_ENGINEER_HEARTBEAT_GUIDANCE}`,
2206
2265
  builtIn: true
2207
2266
  },
2208
2267
  {
@@ -2210,7 +2269,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2210
2269
  name: "Strict Reviewer",
2211
2270
  description: "Rigorous reviewer with stricter criteria for security, edge cases, backward compatibility, and type safety. Issues REVISE more readily.",
2212
2271
  role: "reviewer",
2213
- prompt: STRICT_REVIEWER_PROMPT_TEXT,
2272
+ prompt: `${STRICT_REVIEWER_PROMPT_TEXT}
2273
+
2274
+ ${STRICT_REVIEWER_HEARTBEAT_GUIDANCE}`,
2214
2275
  builtIn: true
2215
2276
  },
2216
2277
  {
@@ -2218,7 +2279,9 @@ Write a PROMPT.md specification to the given path. Be brief and precise \u2014 a
2218
2279
  name: "Concise Triage",
2219
2280
  description: "Shorter, more focused specification format with minimal prose. Produces compact PROMPT.md files with essential information only.",
2220
2281
  role: "triage",
2221
- prompt: CONCISE_TRIAGE_PROMPT_TEXT,
2282
+ prompt: `${CONCISE_TRIAGE_PROMPT_TEXT}
2283
+
2284
+ ${CONCISE_TRIAGE_HEARTBEAT_GUIDANCE}`,
2222
2285
  builtIn: true
2223
2286
  }
2224
2287
  ];
@@ -31992,6 +32055,9 @@ ${newTask.description}
31992
32055
  task.nextRecoveryAt = void 0;
31993
32056
  }
31994
32057
  await this.atomicWriteTaskJson(dir, task);
32058
+ if (toColumn === "done") {
32059
+ this.clearLinkedAgentTaskIds(id, task.updatedAt);
32060
+ }
31995
32061
  if (this.isWatching) this.taskCache.set(id, { ...task });
31996
32062
  this.emit("task:moved", { task, from: fromColumn, to: toColumn });
31997
32063
  return task;
@@ -32699,11 +32765,34 @@ ${task.description}
32699
32765
  }
32700
32766
  rewrittenDependents.push(updatedDependent);
32701
32767
  }
32768
+ this.clearLinkedAgentTaskIds(taskId);
32702
32769
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(taskId);
32703
32770
  this.db.bumpLastModified();
32704
32771
  });
32705
32772
  return rewrittenDependents;
32706
32773
  }
32774
+ /**
32775
+ * Clear `agent.taskId` links that point at a task which has transitioned out
32776
+ * of active work. This keeps heartbeat scheduling aligned with live task
32777
+ * storage and prevents stale task-scoped heartbeat runs.
32778
+ */
32779
+ clearLinkedAgentTaskIds(taskId, updatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
32780
+ const linkedAgents = this.db.prepare("SELECT id FROM agents WHERE taskId = ?").all(taskId);
32781
+ if (linkedAgents.length === 0) {
32782
+ return;
32783
+ }
32784
+ this.db.prepare(`
32785
+ UPDATE agents
32786
+ SET
32787
+ taskId = NULL,
32788
+ updatedAt = ?,
32789
+ data = CASE
32790
+ WHEN json_valid(data) THEN json_set(json_remove(data, '$.taskId'), '$.updatedAt', ?)
32791
+ ELSE data
32792
+ END
32793
+ WHERE taskId = ?
32794
+ `).run(updatedAt, updatedAt, taskId);
32795
+ }
32707
32796
  /**
32708
32797
  * Clean up the git branch associated with a task.
32709
32798
  *
@@ -32985,6 +33074,7 @@ ${task.description}
32985
33074
  });
32986
33075
  if (!cleanup) {
32987
33076
  await this.atomicWriteTaskJson(dir, task);
33077
+ this.clearLinkedAgentTaskIds(id, task.updatedAt);
32988
33078
  if (this.isWatching) this.taskCache.set(id, { ...task });
32989
33079
  this.emit("task:moved", { task, from: "done", to: "archived" });
32990
33080
  return task;
@@ -32998,6 +33088,7 @@ ${task.description}
32998
33088
  }
32999
33089
  const entry = await this.taskToArchiveEntry(task, task.columnMovedAt);
33000
33090
  this.archiveDb.upsert(entry);
33091
+ this.clearLinkedAgentTaskIds(id, task.updatedAt);
33001
33092
  this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
33002
33093
  this.db.bumpLastModified();
33003
33094
  const { rm: rm4 } = await import("node:fs/promises");
@@ -36911,10 +37002,105 @@ async function summarizeCommitBody(diffStat, rootDir, provider, modelId, opts) {
36911
37002
  }
36912
37003
  }
36913
37004
  }
37005
+ async function summarizeCommitSubject(diffStat, rootDir, provider, modelId, opts) {
37006
+ const trimmedStat = (diffStat ?? "").trim();
37007
+ const trimmedCommitLog = (opts?.commitLog ?? "").trim();
37008
+ if (trimmedStat.length === 0 && trimmedCommitLog.length === 0) {
37009
+ return null;
37010
+ }
37011
+ const truncatedStat = trimmedStat.length > MAX_COMMIT_BODY_INPUT_LENGTH ? trimmedStat.slice(0, MAX_COMMIT_BODY_INPUT_LENGTH) + "\n\u2026(truncated)" : trimmedStat;
37012
+ const truncatedCommitLog = trimmedCommitLog.length > MAX_COMMIT_BODY_INPUT_LENGTH ? trimmedCommitLog.slice(0, MAX_COMMIT_BODY_INPUT_LENGTH) + "\n\u2026(truncated)" : trimmedCommitLog;
37013
+ const userPromptParts = [];
37014
+ if (opts?.branch) userPromptParts.push(`Branch: ${opts.branch}`);
37015
+ if (opts?.taskId) userPromptParts.push(`Task: ${opts.taskId}`);
37016
+ if (userPromptParts.length > 0) userPromptParts.push("");
37017
+ if (truncatedCommitLog.length > 0) {
37018
+ userPromptParts.push("Step commits being merged in (most recent first):");
37019
+ userPromptParts.push(truncatedCommitLog);
37020
+ userPromptParts.push("");
37021
+ }
37022
+ if (truncatedStat.length > 0) {
37023
+ userPromptParts.push("Files changed (`git diff --stat`):");
37024
+ userPromptParts.push(truncatedStat);
37025
+ userPromptParts.push("");
37026
+ }
37027
+ userPromptParts.push("Write the commit subject now.");
37028
+ const userPrompt = userPromptParts.join("\n");
37029
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS;
37030
+ const aborter = new AbortController();
37031
+ const timer = setTimeout(() => aborter.abort(), timeoutMs);
37032
+ if (opts?.signal) {
37033
+ if (opts.signal.aborted) aborter.abort();
37034
+ else opts.signal.addEventListener("abort", () => aborter.abort(), { once: true });
37035
+ }
37036
+ let session;
37037
+ try {
37038
+ const createFnAgent5 = await getFnAgent();
37039
+ if (!createFnAgent5) {
37040
+ if (DEBUG) console.log("[ai-summarize] AI engine not available for commit subject");
37041
+ return null;
37042
+ }
37043
+ const agentOptions = {
37044
+ cwd: rootDir,
37045
+ systemPrompt: COMMIT_SUBJECT_SYSTEM_PROMPT,
37046
+ tools: "readonly"
37047
+ };
37048
+ if (provider && modelId) {
37049
+ agentOptions.defaultProvider = provider;
37050
+ agentOptions.defaultModelId = modelId;
37051
+ }
37052
+ const agentResult = await createFnAgent5(agentOptions);
37053
+ if (!agentResult?.session) return null;
37054
+ session = agentResult.session;
37055
+ await session.prompt(userPrompt);
37056
+ if (aborter.signal.aborted) return null;
37057
+ if (session.state?.error) {
37058
+ if (DEBUG) console.log(`[ai-summarize] Commit-subject session error: ${session.state.error}`);
37059
+ return null;
37060
+ }
37061
+ const messages = session.state?.messages ?? [];
37062
+ const assistant = messages.filter((m) => m.role === "assistant").pop();
37063
+ if (!assistant?.content) return null;
37064
+ let raw = "";
37065
+ if (typeof assistant.content === "string") {
37066
+ raw = assistant.content;
37067
+ } else if (Array.isArray(assistant.content)) {
37068
+ raw = assistant.content.filter(
37069
+ (c) => c.type === "text" && typeof c.text === "string"
37070
+ ).map((c) => c.text).join("");
37071
+ }
37072
+ return sanitizeCommitSubject(raw);
37073
+ } catch (err) {
37074
+ if (DEBUG) {
37075
+ const message = err instanceof Error ? err.message : String(err);
37076
+ console.log(`[ai-summarize] Commit-subject generation failed: ${message}`);
37077
+ }
37078
+ return null;
37079
+ } finally {
37080
+ clearTimeout(timer);
37081
+ try {
37082
+ session?.dispose?.();
37083
+ } catch {
37084
+ }
37085
+ }
37086
+ }
37087
+ function sanitizeCommitSubject(raw) {
37088
+ if (!raw) return null;
37089
+ const firstLine = raw.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
37090
+ if (!firstLine) return null;
37091
+ let subject = firstLine.replace(/^[-*]\s+/, "").replace(/^["'`]+|["'`]+$/g, "").trim();
37092
+ subject = subject.replace(/^[a-z]+(?:\([^)]+\))?:\s*/i, "").trim();
37093
+ subject = subject.replace(/\.+$/, "").trim();
37094
+ if (!subject) return null;
37095
+ if (subject.length > MAX_COMMIT_SUBJECT_LENGTH) {
37096
+ subject = subject.slice(0, MAX_COMMIT_SUBJECT_LENGTH).trim();
37097
+ }
37098
+ return subject || null;
37099
+ }
36914
37100
  function __resetSummarizeState() {
36915
37101
  rateLimits.clear();
36916
37102
  }
36917
- var SUMMARIZE_SYSTEM_PROMPT, MAX_DESCRIPTION_LENGTH, MIN_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, MAX_REQUESTS_PER_HOUR, RATE_LIMIT_WINDOW_MS, CLEANUP_INTERVAL_MS, rateLimits, cleanupInterval, ValidationError, RateLimitError, AiServiceError, DEBUG, COMMIT_BODY_SYSTEM_PROMPT, MAX_COMMIT_BODY_INPUT_LENGTH, MAX_COMMIT_BODY_LENGTH, DEFAULT_COMMIT_BODY_TIMEOUT_MS;
37103
+ var SUMMARIZE_SYSTEM_PROMPT, MAX_DESCRIPTION_LENGTH, MIN_DESCRIPTION_LENGTH, MAX_TITLE_LENGTH, MAX_REQUESTS_PER_HOUR, RATE_LIMIT_WINDOW_MS, CLEANUP_INTERVAL_MS, rateLimits, cleanupInterval, ValidationError, RateLimitError, AiServiceError, DEBUG, COMMIT_BODY_SYSTEM_PROMPT, MAX_COMMIT_BODY_INPUT_LENGTH, MAX_COMMIT_BODY_LENGTH, DEFAULT_COMMIT_BODY_TIMEOUT_MS, COMMIT_SUBJECT_SYSTEM_PROMPT, MAX_COMMIT_SUBJECT_LENGTH, DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS;
36918
37104
  var init_ai_summarize = __esm({
36919
37105
  "../core/src/ai-summarize.ts"() {
36920
37106
  "use strict";
@@ -36974,6 +37160,20 @@ Your job is to summarize what landed \u2014 using the branch's step commit subje
36974
37160
  MAX_COMMIT_BODY_INPUT_LENGTH = 4e3;
36975
37161
  MAX_COMMIT_BODY_LENGTH = 2e3;
36976
37162
  DEFAULT_COMMIT_BODY_TIMEOUT_MS = 3e4;
37163
+ COMMIT_SUBJECT_SYSTEM_PROMPT = `You write commit message subjects for merge commits.
37164
+
37165
+ Your job is to summarize what landed \u2014 using the branch's step commit subjects (when provided) and the \`git diff --stat\` \u2014 into a single subject line that conveys the change's essence at a glance.
37166
+
37167
+ ## Guidelines
37168
+ - Output ONLY the subject text \u2014 no quotes, no markdown, no body, no trailing period
37169
+ - Do NOT include any \`feat:\`, \`fix:\`, scope, or task-id prefix \u2014 the caller adds that
37170
+ - Imperative mood ("add X", "fix Y", "refactor Z") and lower-case first word
37171
+ - Hard cap: 60 characters; aim for 40\u201355
37172
+ - Be specific: name the most consequential module/feature/behavior that changed
37173
+ - If the branch has one clear theme, describe it; if it's mixed, lead with the largest change
37174
+ - Do not invent details that aren't in the input`;
37175
+ MAX_COMMIT_SUBJECT_LENGTH = 60;
37176
+ DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS = 15e3;
36977
37177
  }
36978
37178
  });
36979
37179
 
@@ -49154,6 +49354,7 @@ __export(src_exports, {
49154
49354
  COLUMN_DESCRIPTIONS: () => COLUMN_DESCRIPTIONS,
49155
49355
  COLUMN_LABELS: () => COLUMN_LABELS,
49156
49356
  COMMIT_BODY_SYSTEM_PROMPT: () => COMMIT_BODY_SYSTEM_PROMPT,
49357
+ COMMIT_SUBJECT_SYSTEM_PROMPT: () => COMMIT_SUBJECT_SYSTEM_PROMPT,
49157
49358
  COMPACT_MEMORY_SYSTEM_PROMPT: () => COMPACT_MEMORY_SYSTEM_PROMPT,
49158
49359
  CentralCore: () => CentralCore,
49159
49360
  CentralDatabase: () => CentralDatabase,
@@ -49164,6 +49365,7 @@ __export(src_exports, {
49164
49365
  DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS: () => DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS,
49165
49366
  DEFAULT_AUTO_SUMMARIZE_SCHEDULE: () => DEFAULT_AUTO_SUMMARIZE_SCHEDULE,
49166
49367
  DEFAULT_COMMIT_BODY_TIMEOUT_MS: () => DEFAULT_COMMIT_BODY_TIMEOUT_MS,
49368
+ DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS: () => DEFAULT_COMMIT_SUBJECT_TIMEOUT_MS,
49167
49369
  DEFAULT_EXECUTION_MODE: () => DEFAULT_EXECUTION_MODE,
49168
49370
  DEFAULT_GLOBAL_SETTINGS: () => DEFAULT_GLOBAL_SETTINGS,
49169
49371
  DEFAULT_INSIGHT_SCHEDULE: () => DEFAULT_INSIGHT_SCHEDULE,
@@ -49187,6 +49389,7 @@ __export(src_exports, {
49187
49389
  InsightStore: () => InsightStore,
49188
49390
  MAX_COMMIT_BODY_INPUT_LENGTH: () => MAX_COMMIT_BODY_INPUT_LENGTH,
49189
49391
  MAX_COMMIT_BODY_LENGTH: () => MAX_COMMIT_BODY_LENGTH,
49392
+ MAX_COMMIT_SUBJECT_LENGTH: () => MAX_COMMIT_SUBJECT_LENGTH,
49190
49393
  MAX_DESCRIPTION_LENGTH: () => MAX_DESCRIPTION_LENGTH,
49191
49394
  MAX_REQUESTS_PER_HOUR: () => MAX_REQUESTS_PER_HOUR,
49192
49395
  MAX_ROUTINE_RUN_HISTORY: () => MAX_ROUTINE_RUN_HISTORY,
@@ -49423,6 +49626,7 @@ __export(src_exports, {
49423
49626
  runGhAsync: () => runGhAsync,
49424
49627
  runGhJson: () => runGhJson,
49425
49628
  runGhJsonAsync: () => runGhJsonAsync,
49629
+ sanitizeCommitSubject: () => sanitizeCommitSubject,
49426
49630
  scheduleQmdInstallAndRefresh: () => scheduleQmdInstallAndRefresh,
49427
49631
  scheduleQmdProjectMemoryRefresh: () => scheduleQmdProjectMemoryRefresh,
49428
49632
  searchProjectMemory: () => searchProjectMemory,
@@ -49432,6 +49636,7 @@ __export(src_exports, {
49432
49636
  slugify: () => slugify,
49433
49637
  sortTasksByPriorityThenAgeAndId: () => sortTasksByPriorityThenAgeAndId,
49434
49638
  summarizeCommitBody: () => summarizeCommitBody,
49639
+ summarizeCommitSubject: () => summarizeCommitSubject,
49435
49640
  summarizeTitle: () => summarizeTitle,
49436
49641
  syncAutoSummarizeAutomation: () => syncAutoSummarizeAutomation,
49437
49642
  syncBackupAutomation: () => syncBackupAutomation,
@@ -55808,21 +56013,37 @@ function resetMergeWithWarn(rootDir, taskId, label) {
55808
56013
  async function buildDeterministicMergeMessage(params) {
55809
56014
  const { taskId, branch, commitLog, diffStat, includeTaskId, rootDir, settings, signal } = params;
55810
56015
  const prefix = includeTaskId ? `feat(${taskId})` : "feat";
55811
- const subject = `${prefix}: merge ${branch}`;
56016
+ const fallbackSubject = `${prefix}: merge ${branch}`;
55812
56017
  const trimmedCommitLog = commitLog?.trim() ?? "";
55813
56018
  const trimmedDiffStat = diffStat?.trim() ?? "";
55814
56019
  const commitsSection = trimmedCommitLog.length > 0 ? trimmedCommitLog : `- merge ${branch}`;
55815
56020
  let aiSummary = null;
56021
+ let aiSubject = null;
55816
56022
  if (rootDir && settings && (trimmedCommitLog.length > 0 || trimmedDiffStat.length > 0)) {
55817
56023
  const useTitleSummarizer = !!settings.titleSummarizerProvider && !!settings.titleSummarizerModelId;
55818
56024
  const provider = useTitleSummarizer ? settings.titleSummarizerProvider : settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultProviderOverride : settings.defaultProvider;
55819
56025
  const modelId = useTitleSummarizer ? settings.titleSummarizerModelId : settings.defaultProviderOverride && settings.defaultModelIdOverride ? settings.defaultModelIdOverride : settings.defaultModelId;
55820
- aiSummary = await summarizeCommitBody(trimmedDiffStat, rootDir, provider, modelId, {
55821
- branch,
55822
- taskId,
55823
- commitLog: trimmedCommitLog,
55824
- signal
55825
- }).catch(() => null);
56026
+ const [bodyResult, subjectResult] = await Promise.all([
56027
+ summarizeCommitBody(trimmedDiffStat, rootDir, provider, modelId, {
56028
+ branch,
56029
+ taskId,
56030
+ commitLog: trimmedCommitLog,
56031
+ signal
56032
+ }).catch(() => null),
56033
+ summarizeCommitSubject(trimmedDiffStat, rootDir, provider, modelId, {
56034
+ branch,
56035
+ taskId,
56036
+ commitLog: trimmedCommitLog,
56037
+ signal
56038
+ }).catch(() => null)
56039
+ ]);
56040
+ aiSummary = bodyResult;
56041
+ aiSubject = subjectResult;
56042
+ }
56043
+ let subject = fallbackSubject;
56044
+ if (aiSubject && aiSubject.length > 0) {
56045
+ const candidate = `${prefix}: ${aiSubject}`;
56046
+ subject = candidate.length > 72 ? candidate.slice(0, 72).trimEnd() : candidate;
55826
56047
  }
55827
56048
  const sections = [];
55828
56049
  if (aiSummary && aiSummary.trim().length > 0) {
@@ -55883,11 +56104,18 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
55883
56104
  );
55884
56105
  return false;
55885
56106
  }
56107
+ const actualContext = await computeActualMergeCommitContext({
56108
+ rootDir,
56109
+ integrationTargetSha: preAttemptHeadSha,
56110
+ branch
56111
+ });
56112
+ const messageCommitLog = actualContext.commitLog || commitLog;
56113
+ const messageDiffStat = actualContext.diffStat || diffStat;
55886
56114
  const { subjectArg, bodyArg } = await buildDeterministicMergeMessage({
55887
56115
  taskId,
55888
56116
  branch,
55889
- commitLog,
55890
- diffStat,
56117
+ commitLog: messageCommitLog,
56118
+ diffStat: messageDiffStat,
55891
56119
  includeTaskId,
55892
56120
  rootDir,
55893
56121
  settings,
@@ -56339,6 +56567,52 @@ async function collectPatchIds(rootDir, target, windowSize) {
56339
56567
  }
56340
56568
  return ids;
56341
56569
  }
56570
+ async function computeActualMergeCommitContext(params) {
56571
+ const { rootDir, integrationTargetSha, branch } = params;
56572
+ const targetArg = quoteArg(integrationTargetSha);
56573
+ let diffStat = "";
56574
+ try {
56575
+ const { stdout: stagedStat } = await execAsync2(
56576
+ `git diff --cached ${targetArg} --stat`,
56577
+ { cwd: rootDir, encoding: "utf-8" }
56578
+ );
56579
+ diffStat = stagedStat.trim();
56580
+ if (diffStat.length === 0) {
56581
+ const { stdout: headStat } = await execAsync2(
56582
+ `git diff ${targetArg} HEAD --stat`,
56583
+ { cwd: rootDir, encoding: "utf-8" }
56584
+ );
56585
+ diffStat = headStat.trim();
56586
+ }
56587
+ } catch {
56588
+ }
56589
+ let commitLog = "";
56590
+ try {
56591
+ const targetPatchIds = await collectPatchIds(rootDir, integrationTargetSha, 200);
56592
+ const { stdout: branchShas } = await execAsync2(
56593
+ `git log ${targetArg}..${quoteArg(branch)} --format=%H`,
56594
+ { cwd: rootDir, encoding: "utf-8" }
56595
+ );
56596
+ const shas = branchShas.trim().split("\n").filter(Boolean);
56597
+ const lines = [];
56598
+ for (const sha of shas) {
56599
+ const pid = await commitPatchId(rootDir, sha);
56600
+ if (pid && targetPatchIds.has(pid)) continue;
56601
+ try {
56602
+ const { stdout: subj } = await execAsync2(
56603
+ `git log -1 ${quoteArg(sha)} --format=%s`,
56604
+ { cwd: rootDir, encoding: "utf-8" }
56605
+ );
56606
+ const s = subj.trim();
56607
+ if (s) lines.push(`- ${s}`);
56608
+ } catch {
56609
+ }
56610
+ }
56611
+ commitLog = lines.join("\n");
56612
+ } catch {
56613
+ }
56614
+ return { commitLog, diffStat };
56615
+ }
56342
56616
  async function listBranchCommits(rootDir, target, branch) {
56343
56617
  try {
56344
56618
  const { stdout } = await execAsync2(
@@ -57952,11 +58226,25 @@ async function executeMergeAttempt(params, aiTracker) {
57952
58226
  }
57953
58227
  try {
57954
58228
  const authorArg = getCommitAuthorArg(params.settings);
58229
+ let integrationTargetSha;
58230
+ try {
58231
+ const { stdout } = await execAsync2("git rev-parse HEAD~1", {
58232
+ cwd: rootDir,
58233
+ encoding: "utf-8"
58234
+ });
58235
+ integrationTargetSha = stdout.trim() || void 0;
58236
+ } catch {
58237
+ }
58238
+ const actualContext = integrationTargetSha ? await computeActualMergeCommitContext({
58239
+ rootDir,
58240
+ integrationTargetSha,
58241
+ branch
58242
+ }) : { commitLog: "", diffStat: "" };
57955
58243
  const { subjectArg, bodyArg } = await buildDeterministicMergeMessage({
57956
58244
  taskId,
57957
58245
  branch,
57958
- commitLog,
57959
- diffStat,
58246
+ commitLog: actualContext.commitLog || commitLog,
58247
+ diffStat: actualContext.diffStat || diffStat,
57960
58248
  includeTaskId,
57961
58249
  rootDir,
57962
58250
  settings: params.settings,
@@ -64788,30 +65076,27 @@ var init_effective_node = __esm({
64788
65076
  });
64789
65077
 
64790
65078
  // ../engine/src/node-routing-policy.ts
64791
- function applyUnavailableNodePolicy(nodeStatus, policy, isLocal) {
64792
- if (isLocal) {
64793
- return { allowed: true, fallbackToLocal: false, reason: "local-execution" };
64794
- }
64795
- if (nodeStatus === void 0) {
64796
- return { allowed: true, fallbackToLocal: false, reason: "unknown-health" };
65079
+ function applyUnavailableNodePolicy(params) {
65080
+ const { effectiveNode, nodeHealth, policy } = params;
65081
+ if (effectiveNode.source === "local") {
65082
+ return { allowed: true, fallbackToLocal: false };
64797
65083
  }
64798
- if (nodeStatus === "online") {
64799
- return { allowed: true, fallbackToLocal: false, reason: "healthy" };
65084
+ if (nodeHealth === "online" || nodeHealth === void 0) {
65085
+ return { allowed: true, fallbackToLocal: false };
64800
65086
  }
64801
- if (!UNHEALTHY_STATUSES.has(nodeStatus)) {
64802
- return { allowed: true, fallbackToLocal: false, reason: "healthy" };
65087
+ if (!effectiveNode.nodeId || !UNHEALTHY_STATUSES.has(nodeHealth)) {
65088
+ return { allowed: true, fallbackToLocal: false };
64803
65089
  }
64804
65090
  if (policy === "fallback-local") {
64805
65091
  return {
64806
65092
  allowed: true,
64807
65093
  fallbackToLocal: true,
64808
- reason: `fallback-local:${nodeStatus}`
65094
+ reason: `Node ${effectiveNode.nodeId} is ${nodeHealth}; falling back to local per policy`
64809
65095
  };
64810
65096
  }
64811
65097
  return {
64812
65098
  allowed: false,
64813
- fallbackToLocal: false,
64814
- reason: `blocked:${nodeStatus}`
65099
+ reason: `Node ${effectiveNode.nodeId} is ${nodeHealth}; policy is block`
64815
65100
  };
64816
65101
  }
64817
65102
  var UNHEALTHY_STATUSES;
@@ -64986,7 +65271,7 @@ var init_scheduler = __esm({
64986
65271
  /** Tracks mission-linked tasks observed with status=failed before moveTask clears status/error. */
64987
65272
  failedTaskIds = /* @__PURE__ */ new Set();
64988
65273
  /** Tracks tasks blocked by unavailable-node policy to deduplicate block log entries. */
64989
- blockedNodeTaskIds = /* @__PURE__ */ new Set();
65274
+ wasNodeBlocked = /* @__PURE__ */ new Set();
64990
65275
  /**
64991
65276
  * Validate that a task's filesystem state is intact.
64992
65277
  * Checks that the task directory exists and PROMPT.md is present and non-empty.
@@ -65048,7 +65333,7 @@ var init_scheduler = __esm({
65048
65333
  this.options.missionAutopilot.stop();
65049
65334
  }
65050
65335
  this.failedTaskIds.clear();
65051
- this.blockedNodeTaskIds.clear();
65336
+ this.wasNodeBlocked.clear();
65052
65337
  schedulerLog.log("Stopped");
65053
65338
  }
65054
65339
  /**
@@ -65310,35 +65595,24 @@ var init_scheduler = __esm({
65310
65595
  }
65311
65596
  let effectiveNode = resolveEffectiveNode(freshTask, settings);
65312
65597
  schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
65313
- if (effectiveNode.nodeId !== void 0 && this.options.nodeHealthMonitor) {
65314
- const nodeStatus = this.options.nodeHealthMonitor.getNodeHealth(effectiveNode.nodeId);
65315
- const policyResult = applyUnavailableNodePolicy(
65316
- nodeStatus,
65317
- settings.unavailableNodePolicy,
65318
- false
65319
- );
65320
- if (!policyResult.allowed) {
65321
- if (!this.blockedNodeTaskIds.has(task.id)) {
65322
- this.blockedNodeTaskIds.add(task.id);
65323
- schedulerLog.log(
65324
- `Task ${task.id} dispatch blocked \u2014 node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"} (policy: block)`
65325
- );
65326
- await this.store.logEntry(
65327
- task.id,
65328
- `Routing blocked: node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"}, policy=block`
65329
- );
65598
+ if (effectiveNode.nodeId && this.options.nodeHealthMonitor) {
65599
+ const nodeHealth = this.options.nodeHealthMonitor.getNodeHealth(effectiveNode.nodeId);
65600
+ const decision = applyUnavailableNodePolicy({
65601
+ effectiveNode,
65602
+ nodeHealth,
65603
+ policy: settings.unavailableNodePolicy
65604
+ });
65605
+ if (!decision.allowed) {
65606
+ if (!this.wasNodeBlocked.has(task.id)) {
65607
+ this.wasNodeBlocked.add(task.id);
65608
+ schedulerLog.warn(`Task ${task.id} blocked: ${decision.reason}`);
65609
+ await this.store.logEntry(task.id, decision.reason);
65330
65610
  }
65331
65611
  continue;
65332
65612
  }
65333
- this.blockedNodeTaskIds.delete(task.id);
65334
- if (policyResult.fallbackToLocal) {
65335
- schedulerLog.log(
65336
- `Task ${task.id} falling back to local \u2014 node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"} (policy: fallback-local)`
65337
- );
65338
- await this.store.logEntry(
65339
- task.id,
65340
- `Routing fallback to local: node ${effectiveNode.nodeId} is ${nodeStatus ?? "unknown"}, policy=fallback-local`
65341
- );
65613
+ if (decision.fallbackToLocal) {
65614
+ schedulerLog.log(`Task ${task.id} falling back to local: ${decision.reason}`);
65615
+ await this.store.logEntry(task.id, decision.reason);
65342
65616
  effectiveNode = { nodeId: void 0, source: "local" };
65343
65617
  }
65344
65618
  }
@@ -65352,6 +65626,7 @@ var init_scheduler = __esm({
65352
65626
  effectiveNodeSource: effectiveNode.source
65353
65627
  });
65354
65628
  await this.store.moveTask(task.id, "in-progress");
65629
+ this.wasNodeBlocked.delete(task.id);
65355
65630
  await this.store.logEntry(task.id, `Node routing resolved: ${effectiveNode.nodeId ?? "local"} (source: ${effectiveNode.source})`);
65356
65631
  this.options.onSchedule?.(task);
65357
65632
  started++;
@@ -70435,6 +70710,7 @@ When sending messages:
70435
70710
  }
70436
70711
  const agentHasIdentity = hasAgentIdentity(agent);
70437
70712
  const isAgentEphemeral = isEphemeralAgent(agent);
70713
+ const canRunNoTaskHeartbeat = agentHasIdentity && !isAgentEphemeral;
70438
70714
  let taskId = explicitTaskId ?? agent.taskId;
70439
70715
  let inboxSelection = null;
70440
70716
  if (!taskId) {
@@ -70471,7 +70747,7 @@ When sending messages:
70471
70747
  engineRunContext.taskId = taskId;
70472
70748
  }
70473
70749
  if (!taskId) {
70474
- if (!agentHasIdentity || isAgentEphemeral) {
70750
+ if (!canRunNoTaskHeartbeat) {
70475
70751
  heartbeatLog.log(`Agent ${agentId} has no task assignment \u2014 graceful exit`);
70476
70752
  await this.completeRun(agentId, run.id, {
70477
70753
  status: "completed",
@@ -70481,7 +70757,7 @@ When sending messages:
70481
70757
  }
70482
70758
  heartbeatLog.log(`Agent ${agentId} has no task but has identity \u2014 running no-task heartbeat`);
70483
70759
  }
70484
- const isNoTaskRun = !taskId;
70760
+ let isNoTaskRun = !taskId;
70485
70761
  if (!isNoTaskRun) {
70486
70762
  const validStates = ["active", "running", "idle"];
70487
70763
  if (!validStates.includes(agent.state)) {
@@ -70507,53 +70783,99 @@ When sending messages:
70507
70783
  });
70508
70784
  return await this.store.getRunDetail(agentId, run.id);
70509
70785
  }
70510
- if (taskDetail.checkedOutBy && taskDetail.checkedOutBy !== agentId) {
70511
- heartbeatLog.warn(
70512
- `Agent ${agentId} does not hold checkout for ${resolvedTaskId2} (held by ${taskDetail.checkedOutBy}) \u2014 graceful exit`
70513
- );
70514
- await this.completeRun(agentId, run.id, {
70515
- status: "completed",
70516
- resultJson: {
70517
- reason: "checkout_conflict",
70518
- taskId: resolvedTaskId2,
70519
- checkedOutBy: taskDetail.checkedOutBy
70786
+ if (taskDetail.column === "done" || taskDetail.column === "archived") {
70787
+ if (agent.taskId === resolvedTaskId2) {
70788
+ heartbeatLog.log(
70789
+ `Agent ${agentId} linked task ${resolvedTaskId2} is ${taskDetail.column} \u2014 clearing assignment and running heartbeat without task context`
70790
+ );
70791
+ try {
70792
+ await this.store.assignTask(agentId, void 0, runContext);
70793
+ } catch (clearErr) {
70794
+ heartbeatLog.warn(
70795
+ `Failed to clear terminal task assignment ${resolvedTaskId2} for ${agentId}: ${clearErr instanceof Error ? clearErr.message : String(clearErr)}`
70796
+ );
70520
70797
  }
70521
- });
70522
- return await this.store.getRunDetail(agentId, run.id);
70798
+ taskId = void 0;
70799
+ taskDetail = void 0;
70800
+ isNoTaskRun = true;
70801
+ if (!canRunNoTaskHeartbeat) {
70802
+ await this.completeRun(agentId, run.id, {
70803
+ status: "completed",
70804
+ resultJson: { reason: "no_assignment" }
70805
+ });
70806
+ return await this.store.getRunDetail(agentId, run.id);
70807
+ }
70808
+ } else {
70809
+ heartbeatLog.log(
70810
+ `Heartbeat for ${agentId} targeted terminal task ${resolvedTaskId2} (${taskDetail.column}) \u2014 graceful exit`
70811
+ );
70812
+ await this.completeRun(agentId, run.id, {
70813
+ status: "completed",
70814
+ resultJson: { reason: "terminal_task", taskId: resolvedTaskId2, column: taskDetail.column }
70815
+ });
70816
+ return await this.store.getRunDetail(agentId, run.id);
70817
+ }
70523
70818
  }
70524
- const blockedBy = typeof taskDetail.blockedBy === "string" ? taskDetail.blockedBy.trim() : "";
70525
- const isBlockedTask = taskDetail.status === "queued" && blockedBy.length > 0;
70526
- if (isBlockedTask) {
70527
- const commentCount = (taskDetail.comments?.length ?? 0) + (taskDetail.steeringComments?.length ?? 0);
70528
- const lastCommentId = taskDetail.comments?.at(-1)?.id;
70529
- const lastSteeringCommentId = taskDetail.steeringComments?.at(-1)?.id;
70530
- const contextHash = Buffer.from(
70531
- JSON.stringify({ commentCount, lastCommentId, lastSteeringCommentId, blockedBy })
70532
- ).toString("base64").slice(0, 16);
70533
- const currentBlockedState = {
70534
- taskId: resolvedTaskId2,
70535
- blockedBy,
70536
- recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
70537
- contextHash
70538
- };
70539
- const previousBlockedState = await this.store.getLastBlockedState(agentId);
70540
- if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
70819
+ if (isNoTaskRun) {
70820
+ heartbeatLog.log(`Agent ${agentId} terminal task assignment resolved into no-task heartbeat`);
70821
+ } else {
70822
+ const liveTaskDetail = taskDetail;
70823
+ if (!liveTaskDetail) {
70824
+ heartbeatLog.warn(`Task ${resolvedTaskId2} lost detail after terminal-assignment handling \u2014 graceful exit`);
70541
70825
  await this.completeRun(agentId, run.id, {
70542
70826
  status: "completed",
70543
- resultJson: { reason: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
70827
+ resultJson: { reason: "task_not_found", taskId: resolvedTaskId2 }
70828
+ });
70829
+ return await this.store.getRunDetail(agentId, run.id);
70830
+ }
70831
+ if (liveTaskDetail.checkedOutBy && liveTaskDetail.checkedOutBy !== agentId) {
70832
+ heartbeatLog.warn(
70833
+ `Agent ${agentId} does not hold checkout for ${resolvedTaskId2} (held by ${liveTaskDetail.checkedOutBy}) \u2014 graceful exit`
70834
+ );
70835
+ await this.completeRun(agentId, run.id, {
70836
+ status: "completed",
70837
+ resultJson: {
70838
+ reason: "checkout_conflict",
70839
+ taskId: resolvedTaskId2,
70840
+ checkedOutBy: liveTaskDetail.checkedOutBy
70841
+ }
70842
+ });
70843
+ return await this.store.getRunDetail(agentId, run.id);
70844
+ }
70845
+ const blockedBy = typeof liveTaskDetail.blockedBy === "string" ? liveTaskDetail.blockedBy.trim() : "";
70846
+ const isBlockedTask = liveTaskDetail.status === "queued" && blockedBy.length > 0;
70847
+ if (isBlockedTask) {
70848
+ const commentCount = (liveTaskDetail.comments?.length ?? 0) + (liveTaskDetail.steeringComments?.length ?? 0);
70849
+ const lastCommentId = liveTaskDetail.comments?.at(-1)?.id;
70850
+ const lastSteeringCommentId = liveTaskDetail.steeringComments?.at(-1)?.id;
70851
+ const contextHash = Buffer.from(
70852
+ JSON.stringify({ commentCount, lastCommentId, lastSteeringCommentId, blockedBy })
70853
+ ).toString("base64").slice(0, 16);
70854
+ const currentBlockedState = {
70855
+ taskId: resolvedTaskId2,
70856
+ blockedBy,
70857
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
70858
+ contextHash
70859
+ };
70860
+ const previousBlockedState = await this.store.getLastBlockedState(agentId);
70861
+ if (previousBlockedState && isBlockedStateDuplicate(currentBlockedState, previousBlockedState)) {
70862
+ await this.completeRun(agentId, run.id, {
70863
+ status: "completed",
70864
+ resultJson: { reason: "blocked_duplicate", taskId: resolvedTaskId2, blockedBy }
70865
+ });
70866
+ return await this.store.getRunDetail(agentId, run.id);
70867
+ }
70868
+ const blockedMessage = `Task is blocked by ${blockedBy}; waiting for dependency/context changes before retrying.`;
70869
+ await taskStore.addComment(resolvedTaskId2, blockedMessage, "agent", void 0, runContext);
70870
+ await audit.database({ type: "task:comment:add", target: resolvedTaskId2, metadata: { blockedBy } });
70871
+ await this.store.setLastBlockedState(agentId, currentBlockedState);
70872
+ heartbeatLog.log(`Task ${resolvedTaskId2} is blocked by ${blockedBy} \u2014 recorded blocked state`);
70873
+ await this.completeRun(agentId, run.id, {
70874
+ status: "completed",
70875
+ resultJson: { reason: "blocked", taskId: resolvedTaskId2, blockedBy }
70544
70876
  });
70545
70877
  return await this.store.getRunDetail(agentId, run.id);
70546
70878
  }
70547
- const blockedMessage = `Task is blocked by ${blockedBy}; waiting for dependency/context changes before retrying.`;
70548
- await taskStore.addComment(resolvedTaskId2, blockedMessage, "agent", void 0, runContext);
70549
- await audit.database({ type: "task:comment:add", target: resolvedTaskId2, metadata: { blockedBy } });
70550
- await this.store.setLastBlockedState(agentId, currentBlockedState);
70551
- heartbeatLog.log(`Task ${resolvedTaskId2} is blocked by ${blockedBy} \u2014 recorded blocked state`);
70552
- await this.completeRun(agentId, run.id, {
70553
- status: "completed",
70554
- resultJson: { reason: "blocked", taskId: resolvedTaskId2, blockedBy }
70555
- });
70556
- return await this.store.getRunDetail(agentId, run.id);
70557
70879
  }
70558
70880
  }
70559
70881
  if (!isNoTaskRun) {
@@ -78665,6 +78987,7 @@ __export(src_exports2, {
78665
78987
  WebhookNotificationProvider: () => WebhookNotificationProvider,
78666
78988
  WorktreePool: () => WorktreePool,
78667
78989
  aiMergeTask: () => aiMergeTask,
78990
+ applyUnavailableNodePolicy: () => applyUnavailableNodePolicy,
78668
78991
  buildAgentChatPrompt: () => buildAgentChatPrompt,
78669
78992
  buildNtfyClickUrl: () => buildNtfyClickUrl,
78670
78993
  buildRuntimeResolutionContext: () => buildRuntimeResolutionContext,
@@ -78748,6 +79071,7 @@ var init_src2 = __esm({
78748
79071
  init_project_engine();
78749
79072
  init_project_engine_manager();
78750
79073
  init_node_health_monitor();
79074
+ init_node_routing_policy();
78751
79075
  init_peer_exchange_service();
78752
79076
  init_remote_access();
78753
79077
  init_remote_node_client();