@runfusion/fusion 0.0.5 → 0.0.6

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.
@@ -78,11 +78,11 @@
78
78
  }
79
79
  })();
80
80
  </script>
81
- <script type="module" crossorigin src="/assets/index-wtVkS5Qz.js"></script>
81
+ <script type="module" crossorigin src="/assets/index-zTogbMzz.js"></script>
82
82
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-K0fH_qHe.js">
83
83
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DzcZoU0P.js">
84
84
  <link rel="stylesheet" crossorigin href="/assets/vendor-xterm-LZoznX6r.css">
85
- <link rel="stylesheet" crossorigin href="/assets/index-9GdD09x7.css">
85
+ <link rel="stylesheet" crossorigin href="/assets/index-CYkQfLYV.css">
86
86
  </head>
87
87
  <body>
88
88
  <div id="root"></div>
package/dist/extension.js CHANGED
@@ -133,6 +133,8 @@ var init_settings_schema = __esm({
133
133
  defaultPresetBySize: {},
134
134
  autoResolveConflicts: true,
135
135
  smartConflictResolution: true,
136
+ worktreeRebaseBeforeMerge: true,
137
+ worktreeRebaseRemote: "",
136
138
  strictScopeEnforcement: false,
137
139
  buildRetryCount: 0,
138
140
  verificationFixRetries: 1,
@@ -30548,6 +30550,7 @@ ${newTask.description}
30548
30550
  }
30549
30551
  }
30550
30552
  if (updates.steps !== void 0) task.steps = updates.steps;
30553
+ if (updates.currentStep !== void 0) task.currentStep = updates.currentStep;
30551
30554
  if (updates.status === null) {
30552
30555
  task.status = void 0;
30553
30556
  } else if (updates.status !== void 0) {
@@ -53420,6 +53423,71 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
53420
53423
  mergerLog.warn(`${taskId}: unable to verify/checkout main branch \u2014 proceeding on current HEAD`);
53421
53424
  }
53422
53425
  }
53426
+ if (settings.worktreeRebaseBeforeMerge !== false) {
53427
+ try {
53428
+ let remote = settings.worktreeRebaseRemote?.trim();
53429
+ if (!remote) {
53430
+ try {
53431
+ const { stdout: mainBranchOut } = await execAsync2(
53432
+ "git rev-parse --abbrev-ref HEAD",
53433
+ { cwd: rootDir, encoding: "utf-8" }
53434
+ );
53435
+ const mainBranch = mainBranchOut.trim();
53436
+ const { stdout: configuredRemote } = await execAsync2(
53437
+ `git config --get branch.${mainBranch}.remote`,
53438
+ { cwd: rootDir, encoding: "utf-8" }
53439
+ ).catch(() => ({ stdout: "" }));
53440
+ remote = configuredRemote.trim();
53441
+ } catch {
53442
+ }
53443
+ }
53444
+ if (!remote) {
53445
+ try {
53446
+ const { stdout: remotesOut } = await execAsync2("git remote", {
53447
+ cwd: rootDir,
53448
+ encoding: "utf-8"
53449
+ });
53450
+ const remotes = remotesOut.trim().split(/\s+/).filter(Boolean);
53451
+ if (remotes.length === 1) {
53452
+ remote = remotes[0];
53453
+ } else if (remotes.includes("origin")) {
53454
+ remote = "origin";
53455
+ }
53456
+ } catch {
53457
+ }
53458
+ }
53459
+ if (!remote) {
53460
+ mergerLog.log(`${taskId}: no remote resolvable \u2014 skipping pre-merge rebase`);
53461
+ } else {
53462
+ mergerLog.log(`${taskId}: fetching ${remote} before merge`);
53463
+ await execAsync2(`git fetch "${remote}"`, { cwd: rootDir });
53464
+ try {
53465
+ const { stdout: mainBranchOut } = await execAsync2(
53466
+ "git rev-parse --abbrev-ref HEAD",
53467
+ { cwd: rootDir, encoding: "utf-8" }
53468
+ );
53469
+ const mainBranch = mainBranchOut.trim();
53470
+ const remoteRef = `${remote}/${mainBranch}`;
53471
+ if (worktreePath) {
53472
+ await execAsync2(`git rebase "${remoteRef}"`, { cwd: worktreePath });
53473
+ mergerLog.log(`${taskId}: rebased ${branch} onto ${remoteRef}`);
53474
+ } else {
53475
+ mergerLog.warn(`${taskId}: no worktreePath \u2014 skipping task branch rebase`);
53476
+ }
53477
+ } catch (rebaseErr) {
53478
+ const msg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
53479
+ mergerLog.warn(`${taskId}: pre-merge rebase failed (${msg}) \u2014 aborting rebase and falling through to smart/AI merge`);
53480
+ if (worktreePath) {
53481
+ await execAsync2("git rebase --abort", { cwd: worktreePath }).catch(() => {
53482
+ });
53483
+ }
53484
+ }
53485
+ }
53486
+ } catch (err) {
53487
+ const msg = err instanceof Error ? err.message : String(err);
53488
+ mergerLog.warn(`${taskId}: pre-merge rebase pipeline failed (${msg}) \u2014 proceeding without rebase`);
53489
+ }
53490
+ }
53423
53491
  let commitLog = "";
53424
53492
  let diffStat = "";
53425
53493
  try {
@@ -55936,19 +56004,6 @@ import { existsSync as existsSync25 } from "node:fs";
55936
56004
  import { readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
55937
56005
  import { Type as Type4 } from "@mariozechner/pi-ai";
55938
56006
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
55939
- function determineRevisionResetStart(steps, feedback) {
55940
- const total = steps.length;
55941
- if (total === 0) return 0;
55942
- const skipPreflight = /preflight/i.test(steps[0].name);
55943
- const firstCandidate = skipPreflight ? 1 : 0;
55944
- if (firstCandidate >= total) return total;
55945
- const fb = feedback.toLowerCase();
55946
- for (let i = firstCandidate; i < total; i++) {
55947
- const tokens = steps[i].name.toLowerCase().match(/[a-z][a-z]{4,}/g) ?? [];
55948
- if (tokens.some((t) => fb.includes(t))) return i;
55949
- }
55950
- return firstCandidate;
55951
- }
55952
56007
  function truncateWorkflowScriptOutput2(output) {
55953
56008
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2) return output;
55954
56009
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2} characters ...
@@ -58265,35 +58320,23 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
58265
58320
  }
58266
58321
  /**
58267
58322
  * Handle a workflow step revision request.
58268
- *
58269
- * This method:
58270
- * 1. Updates PROMPT.md with "Workflow Revision Instructions" section
58271
- * 2. Resets task execution state (all steps reset to pending)
58272
- * 3. Schedules fresh execution to run after current guard unwinds
58273
- *
58274
- * The task stays in "in-progress" and is scheduled for a fresh executor pass.
58323
+ *
58324
+ * Re-opens ONLY the last step so the executor has exactly one pending slot
58325
+ * to re-enter through. All earlier done steps stay done — the agent reads
58326
+ * the injected feedback from PROMPT.md and applies an in-place fix rather
58327
+ * than redoing any completed step.
58275
58328
  */
58276
58329
  async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
58277
58330
  executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
58278
58331
  const updatedTask = await this.store.getTask(task.id);
58279
- const resetStart = determineRevisionResetStart(updatedTask.steps, feedback);
58280
- const targetStepName = resetStart < updatedTask.steps.length ? updatedTask.steps[resetStart].name : null;
58281
- const resetSummary = resetStart >= updatedTask.steps.length ? "no steps to reset" : `resetting steps ${resetStart + 1}\u2013${updatedTask.steps.length} (starting at "${targetStepName}")`;
58332
+ const reopen = await this.reopenLastStepForRevision(task.id, updatedTask);
58333
+ const reopenSummary = reopen ? `re-opening Step ${reopen.index + 1} ("${reopen.name}") for in-place fix` : "no step to re-open (none were completed)";
58282
58334
  await this.store.logEntry(
58283
58335
  task.id,
58284
- `Workflow step "${stepName}" requested revision \u2014 ${resetSummary}`,
58336
+ `Workflow step "${stepName}" requested revision \u2014 ${reopenSummary}`,
58285
58337
  feedback
58286
58338
  );
58287
- await this.injectWorkflowRevisionInstructions(task, feedback, {
58288
- resetStart,
58289
- targetStepName,
58290
- totalSteps: updatedTask.steps.length
58291
- });
58292
- for (let i = resetStart; i < updatedTask.steps.length; i++) {
58293
- if (updatedTask.steps[i].status !== "pending") {
58294
- await this.store.updateStep(task.id, i, "pending");
58295
- }
58296
- }
58339
+ await this.injectWorkflowRevisionInstructions(task, feedback);
58297
58340
  await this.store.updateTask(task.id, {
58298
58341
  status: null,
58299
58342
  sessionFile: null
@@ -58315,12 +58358,36 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
58315
58358
  }
58316
58359
  }, 0);
58317
58360
  }
58361
+ /**
58362
+ * Re-open the last non-pending step so a revision/failure handler gives the
58363
+ * executor exactly one pending slot to re-enter through. Returns the index
58364
+ * and name of the step that was flipped to `pending`, or null when there
58365
+ * was nothing to re-open.
58366
+ */
58367
+ async reopenLastStepForRevision(taskId, task) {
58368
+ const steps = task.steps;
58369
+ if (steps.length === 0) return null;
58370
+ let targetIndex = -1;
58371
+ for (let i = steps.length - 1; i >= 0; i--) {
58372
+ if (steps[i].status !== "pending") {
58373
+ targetIndex = i;
58374
+ break;
58375
+ }
58376
+ }
58377
+ if (targetIndex === -1) {
58378
+ await this.store.updateTask(taskId, { currentStep: 0 });
58379
+ return null;
58380
+ }
58381
+ await this.store.updateStep(taskId, targetIndex, "pending");
58382
+ await this.store.updateTask(taskId, { currentStep: targetIndex });
58383
+ return { index: targetIndex, name: steps[targetIndex].name };
58384
+ }
58318
58385
  /**
58319
58386
  * Inject or update the "Workflow Revision Instructions" section in PROMPT.md.
58320
58387
  * This section contains feedback from workflow steps that requested revisions.
58321
58388
  * The section is replaced entirely to avoid accumulation of old feedback.
58322
58389
  */
58323
- async injectWorkflowRevisionInstructions(task, feedback, scope) {
58390
+ async injectWorkflowRevisionInstructions(task, feedback) {
58324
58391
  const promptPath = join32(this.store.getFusionDir(), "tasks", task.id, "PROMPT.md");
58325
58392
  let content;
58326
58393
  try {
@@ -58329,14 +58396,7 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
58329
58396
  executorLog.warn(`${task.id}: PROMPT.md not found at ${promptPath}, skipping revision injection`);
58330
58397
  return;
58331
58398
  }
58332
- let scopeLine;
58333
- if (scope && scope.targetStepName && scope.resetStart < scope.totalSteps) {
58334
- scopeLine = `Re-execution starts at **Step ${scope.resetStart + 1} ("${scope.targetStepName}")**. Earlier steps remain done \u2014 do not re-run them unless the feedback explicitly calls them out.`;
58335
- } else if (scope && scope.resetStart >= scope.totalSteps) {
58336
- scopeLine = "No steps were reset; apply the feedback as an in-place fix and call task_done() when complete.";
58337
- } else {
58338
- scopeLine = "Address the feedback above by making the necessary code changes, then mark all affected steps as done and call task_done() when complete.";
58339
- }
58399
+ const scopeLine = "All prior steps remain **done**. Apply the feedback above as an in-place fix (make the necessary code changes, commit, and call `task_done()` when complete). Do **not** re-run or re-plan any earlier step unless the feedback explicitly calls it out.";
58340
58400
  const revisionSectionHeader = "## Workflow Revision Instructions";
58341
58401
  const revisionSectionContent = `${revisionSectionHeader}
58342
58402
 
@@ -58395,11 +58455,7 @@ ${feedback}
58395
58455
  });
58396
58456
  await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, retryCount);
58397
58457
  const updatedTask = await this.store.getTask(task.id);
58398
- for (let i = 0; i < updatedTask.steps.length; i++) {
58399
- if (updatedTask.steps[i].status !== "pending") {
58400
- await this.store.updateStep(task.id, i, "pending");
58401
- }
58402
- }
58458
+ await this.reopenLastStepForRevision(task.id, updatedTask);
58403
58459
  await this.store.updateTask(task.id, {
58404
58460
  status: null,
58405
58461
  sessionFile: null
@@ -58443,11 +58499,7 @@ Please fix the issues so the verification can pass on the next attempt.`,
58443
58499
  );
58444
58500
  await this.injectWorkflowStepFailureInstructions(task, failureFeedback, stepName, MAX_WORKFLOW_STEP_RETRIES);
58445
58501
  const updatedTask = await this.store.getTask(taskId);
58446
- for (let i = 0; i < updatedTask.steps.length; i++) {
58447
- if (updatedTask.steps[i].status !== "pending") {
58448
- await this.store.updateStep(taskId, i, "pending");
58449
- }
58450
- }
58502
+ await this.reopenLastStepForRevision(taskId, updatedTask);
58451
58503
  await this.store.updateTask(taskId, {
58452
58504
  status: null,
58453
58505
  error: null,
@@ -64246,6 +64298,12 @@ async function getHeartbeatMemorySettings(taskStore) {
64246
64298
  }
64247
64299
  return maybeGetSettings.call(taskStore);
64248
64300
  }
64301
+ function isTickableState(state) {
64302
+ return state === "active" || state === "running";
64303
+ }
64304
+ function isHeartbeatManaged(agent) {
64305
+ return !isEphemeralAgent(agent);
64306
+ }
64249
64307
  var HEARTBEAT_SYSTEM_PROMPT, HEARTBEAT_NO_TASK_SYSTEM_PROMPT, heartbeatDoneParams, HeartbeatMonitor, HeartbeatTriggerScheduler;
64250
64308
  var init_agent_heartbeat = __esm({
64251
64309
  "../engine/src/agent-heartbeat.ts"() {
@@ -65482,10 +65540,6 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65482
65540
  * @param config - Per-agent heartbeat config
65483
65541
  */
65484
65542
  registerAgent(agentId, config) {
65485
- if (config.enabled === false) {
65486
- heartbeatLog.log(`Skipping timer registration for ${agentId} (disabled)`);
65487
- return;
65488
- }
65489
65543
  let rawIntervalMs = config.heartbeatIntervalMs;
65490
65544
  let usingDefaultInterval = false;
65491
65545
  if (!rawIntervalMs || typeof rawIntervalMs !== "number" || !Number.isFinite(rawIntervalMs) || rawIntervalMs <= 0) {
@@ -65575,8 +65629,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65575
65629
  this.assignedListener = async (agent, taskId) => {
65576
65630
  if (!this.running) return;
65577
65631
  try {
65578
- if (agent.runtimeConfig?.enabled === false) {
65579
- heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (heartbeat disabled)`);
65632
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
65633
+ heartbeatLog.log(`Assignment trigger skipped for ${agent.id} (state=${agent.state})`);
65580
65634
  return;
65581
65635
  }
65582
65636
  const activeRun = await this.store.getActiveHeartbeatRun(agent.id);
@@ -65645,9 +65699,21 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65645
65699
  watchAgentLifecycle() {
65646
65700
  if (this.updatedListener || this.deletedListener) return;
65647
65701
  this.updatedListener = (agent) => {
65648
- if (agent.state === "terminated" || agent.runtimeConfig?.enabled === false) {
65702
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
65649
65703
  this.unregisterAgent(agent.id);
65704
+ return;
65650
65705
  }
65706
+ if (this.timers.has(agent.id)) {
65707
+ return;
65708
+ }
65709
+ const rc = agent.runtimeConfig ?? {};
65710
+ this.registerAgent(agent.id, {
65711
+ heartbeatIntervalMs: rc.heartbeatIntervalMs,
65712
+ maxConcurrentRuns: rc.maxConcurrentRuns
65713
+ });
65714
+ heartbeatLog.log(
65715
+ `State-driven registration: ${agent.id} is ${agent.state} \u2014 timer armed`
65716
+ );
65651
65717
  };
65652
65718
  this.deletedListener = (agentId) => {
65653
65719
  this.unregisterAgent(agentId);
@@ -65678,8 +65744,8 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
65678
65744
  this.unregisterAgent(agentId);
65679
65745
  return;
65680
65746
  }
65681
- if (agent.state === "terminated" || agent.runtimeConfig?.enabled === false) {
65682
- heartbeatLog.log(`Timer tick skipped for ${agentId} (disabled or terminated)`);
65747
+ if (!isHeartbeatManaged(agent) || !isTickableState(agent.state)) {
65748
+ heartbeatLog.log(`Timer tick skipped for ${agentId} (state=${agent.state})`);
65683
65749
  this.unregisterAgent(agentId);
65684
65750
  return;
65685
65751
  }
@@ -67884,13 +67950,13 @@ var init_in_process_runtime = __esm({
67884
67950
  this.taskStore
67885
67951
  );
67886
67952
  this.triggerScheduler.start();
67953
+ const isTickable = (agent) => !isEphemeralAgent(agent) && (agent.state === "active" || agent.state === "running");
67887
67954
  this.agentCreatedListener = (agent) => {
67888
67955
  if (!this.triggerScheduler) return;
67956
+ if (!isTickable(agent)) return;
67889
67957
  const rc = agent.runtimeConfig;
67890
- if (rc?.enabled === false) return;
67891
67958
  this.triggerScheduler.registerAgent(agent.id, {
67892
67959
  heartbeatIntervalMs: rc?.heartbeatIntervalMs,
67893
- enabled: rc?.enabled,
67894
67960
  maxConcurrentRuns: rc?.maxConcurrentRuns
67895
67961
  });
67896
67962
  runtimeLog.log(`Registered new agent ${agent.id} for heartbeat triggers`);
@@ -67898,18 +67964,17 @@ var init_in_process_runtime = __esm({
67898
67964
  this.agentStore.on("agent:created", this.agentCreatedListener);
67899
67965
  this.agentUpdatedListener = (agent) => {
67900
67966
  if (!this.triggerScheduler) return;
67901
- const rc = agent.runtimeConfig;
67902
- if (rc?.enabled === false) {
67967
+ if (!isTickable(agent)) {
67903
67968
  this.triggerScheduler.unregisterAgent(agent.id);
67904
- runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (disabled)`);
67905
- } else {
67906
- this.triggerScheduler.registerAgent(agent.id, {
67907
- heartbeatIntervalMs: rc?.heartbeatIntervalMs,
67908
- enabled: rc?.enabled,
67909
- maxConcurrentRuns: rc?.maxConcurrentRuns
67910
- });
67911
- runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers`);
67969
+ runtimeLog.log(`Unregistered agent ${agent.id} from heartbeat triggers (state=${agent.state})`);
67970
+ return;
67912
67971
  }
67972
+ const rc = agent.runtimeConfig;
67973
+ this.triggerScheduler.registerAgent(agent.id, {
67974
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
67975
+ maxConcurrentRuns: rc?.maxConcurrentRuns
67976
+ });
67977
+ runtimeLog.log(`Re-registered agent ${agent.id} for heartbeat triggers (state=${agent.state})`);
67913
67978
  };
67914
67979
  this.agentStore.on("agent:updated", this.agentUpdatedListener);
67915
67980
  this.ephemeralTerminationListener = (agentId, from, to) => {
@@ -67944,15 +68009,13 @@ var init_in_process_runtime = __esm({
67944
68009
  const agents = await this.agentStore.listAgents();
67945
68010
  let registeredCount = 0;
67946
68011
  for (const agent of agents) {
68012
+ if (!isTickable(agent)) continue;
67947
68013
  const rc = agent.runtimeConfig;
67948
- if (rc?.enabled !== false) {
67949
- this.triggerScheduler.registerAgent(agent.id, {
67950
- heartbeatIntervalMs: rc?.heartbeatIntervalMs,
67951
- enabled: rc?.enabled,
67952
- maxConcurrentRuns: rc?.maxConcurrentRuns
67953
- });
67954
- registeredCount++;
67955
- }
68014
+ this.triggerScheduler.registerAgent(agent.id, {
68015
+ heartbeatIntervalMs: rc?.heartbeatIntervalMs,
68016
+ maxConcurrentRuns: rc?.maxConcurrentRuns
68017
+ });
68018
+ registeredCount++;
67956
68019
  }
67957
68020
  if (agents.length > 0) {
67958
68021
  runtimeLog.log(`Registered ${registeredCount} of ${agents.length} agents for heartbeat triggers`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runfusion/fusion",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "license": "MIT",
5
5
  "description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.",
6
6
  "homepage": "https://github.com/Runfusion/Fusion#readme",