## Summary
First wave of package-internal code organization: split oversized
modules into domain-named files/folders while preserving public import
paths via re-exports, and refresh the line-count ratchet scoreboard.
- **Plan:**
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`
(multi-wave program; this PR lands U1–U4 + first U3/U6 slices)
- **Core types:** peel `types.ts` into
`types/{board,merge-queue,execution-and-ui,merge-policy,workflow-steps}.ts`
with browser-safe Vite alias preserved
- **Core TaskStore:** rename `remaining-ops-9` →
`task-commit-associations` (domain-named, not ordinal dump)
- **Engine executor:** peel pure helpers into
`executor/{browser-probe,requeue-loop,pseudo-pause,workflow-step-failures}.ts`
- **Engine heartbeat:** peel system prompts/procedures into
`agent-heartbeat-prompts.ts`
- **Ratchet:** one-time baseline truth-up + ratchet-down for touched
files
### Deferred to follow-up PRs (plan U5, U7–U9 + remaining waves)
- Self-healing folder split
- Further remaining-ops domain peels
- Dashboard `legacy.ts` / routes / UI monofiles
- CLI extension + TUI peels
## Test plan
- [x] `pnpm --filter @fusion/core exec tsc --noEmit`
- [x] `pnpm --filter @fusion/engine exec tsc --noEmit`
- [x] Focused vitest: `detect-pseudo-pause`,
`executor-browser-verification`, `clear-terminal-workflow-step-failures`
- [x] `node scripts/check-file-line-count.mjs` clean against updated
baseline
- [ ] CI merge gate (lint/typecheck/build/gate)
- [ ] Browser smoke: N/A for this PR (no dashboard UI route changes)
## Residual Review Findings
None. Review autofix applied dual-home wiring for
`clearTerminalWorkflowStepFailures` only.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added configurable heartbeat procedures for task and no-task scenarios
(including patrol-aware rendering).
* Improved agent-browser availability verification with clearer
availability/status reporting.
* Added detection for pseudo-pauses and review-handoff requests.
* Expanded core configuration/contract options for
execution/UI/localization, merges, merge queues, and workflow steps.
* **Bug Fixes**
* Improved handling of transient execute-requeue and workflow-step
retry/cleanup behavior, including better Windows path support.
* Preserved existing public interfaces during internal restructuring.
* **Documentation**
* Added a multi-phase roadmap for future package reorganization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
106 lines
5.0 KiB
TypeScript
106 lines
5.0 KiB
TypeScript
/**
|
|
* FNXC:CodeOrganization 2026-07-15-00:00:
|
|
* Execute-node self-requeue loop signature helpers peeled from executor.ts.
|
|
*/
|
|
import type { TaskDetail, Task } from "@fusion/core";
|
|
import { TaskDeletedError } from "@fusion/core";
|
|
import { isAbsolute, relative, resolve as resolvePath } from "node:path";
|
|
|
|
/** Maximum no-progress execute-node self-requeues before terminalizing the loop. */
|
|
export const MAX_EXECUTE_REQUEUE_LOOP_CYCLES = 6;
|
|
/** Low-water mark for surfacing a visible warning before loop terminalization. */
|
|
export const EXECUTE_REQUEUE_LOOP_VISIBLE_THRESHOLD = 3;
|
|
|
|
function countExecuteRequeueTerminalSteps(live: TaskDetail): number {
|
|
return live.steps?.filter((step) => step.status === "done" || step.status === "skipped").length ?? 0;
|
|
}
|
|
|
|
function parseExecuteRequeueLoopProgressSignature(signature: string | null | undefined): { terminalStepCount: number; totalSteps: number } | null {
|
|
if (!signature) return null;
|
|
try {
|
|
const parsed = JSON.parse(signature) as { terminalStepCount?: unknown; totalSteps?: unknown };
|
|
if (typeof parsed.terminalStepCount !== "number" || typeof parsed.totalSteps !== "number") return null;
|
|
return {
|
|
terminalStepCount: parsed.terminalStepCount,
|
|
totalSteps: parsed.totalSteps,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function buildExecuteRequeueLoopSignature(live: TaskDetail): string {
|
|
/*
|
|
FNXC:WorkflowLifecycle 2026-07-13-07:42:
|
|
FN-7941: human reports #2043/#2045/#2046/#2047 showed that FN-7863's raw currentStep/status signature could drift on every execute self-requeue while no step reached a terminal state, resetting the loop counter to 1 forever. Anchor the bounded streak to monotonic terminal-step progress instead: pending/in-progress/currentStep oscillation still counts toward exhaustion, while real done/skipped progress resets the streak and FN-7926 still diverts completed-blocked work before this guard can fail it.
|
|
*/
|
|
return JSON.stringify({
|
|
terminalStepCount: countExecuteRequeueTerminalSteps(live),
|
|
totalSteps: live.steps?.length ?? 0,
|
|
});
|
|
}
|
|
|
|
export function buildExecuteRequeueLoopHighWaterSignature(live: TaskDetail, previousSignature: string | null | undefined): { signature: string; madeForwardProgress: boolean } {
|
|
// FNXC:WorkflowLifecycle 2026-07-13-08:20: derive current terminal-step
|
|
// progress by parsing buildExecuteRequeueLoopSignature's own output rather
|
|
// than duplicating countExecuteRequeueTerminalSteps/totalSteps inline, so
|
|
// the two functions cannot silently drift out of sync.
|
|
const current = parseExecuteRequeueLoopProgressSignature(buildExecuteRequeueLoopSignature(live));
|
|
const currentTerminalStepCount = current?.terminalStepCount ?? countExecuteRequeueTerminalSteps(live);
|
|
const totalSteps = current?.totalSteps ?? (live.steps?.length ?? 0);
|
|
const previous = parseExecuteRequeueLoopProgressSignature(previousSignature);
|
|
const previousTerminalStepCount = previous?.terminalStepCount ?? currentTerminalStepCount;
|
|
const madeForwardProgress = previous != null && currentTerminalStepCount > previousTerminalStepCount;
|
|
return {
|
|
madeForwardProgress,
|
|
signature: JSON.stringify({
|
|
terminalStepCount: Math.max(previousTerminalStepCount, currentTerminalStepCount),
|
|
totalSteps,
|
|
}),
|
|
};
|
|
}
|
|
|
|
const INVALID_ASSISTANT_CONTINUATION_PATTERN = /cannot continue from message role:\s*assistant/i;
|
|
|
|
export function isInvalidAssistantContinuationErrorMessage(errorMessage: string): boolean {
|
|
return INVALID_ASSISTANT_CONTINUATION_PATTERN.test(errorMessage);
|
|
}
|
|
|
|
export const TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN = /ENOENT:\s+no such file or directory,\s+open\s+'([^']+[\\/]\.fusion[\\/]tasks[\\/]([^\\/]+)[\\/]task\.json)'/;
|
|
|
|
function normalizeErrorPath(path: string): string {
|
|
return resolvePath(path.replace(/\\/g, "/"));
|
|
}
|
|
|
|
function isContainedByWorktree(filePath: string, worktree: string): boolean {
|
|
/*
|
|
FNXC:ExecutorRecovery 2026-07-15-13:20:
|
|
Transient task.json recovery must recognize Node ENOENT paths on both Windows and POSIX, but only when the missing file belongs to the task's exact worktree. Normalize separator-only error-message differences before path resolution, then use relative-path containment so sibling prefixes such as `fn-1-copy` cannot be mistaken for `fn-1`.
|
|
*/
|
|
const relativePath = relative(normalizeErrorPath(worktree), normalizeErrorPath(filePath));
|
|
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
}
|
|
|
|
export function isTransientMissingTaskJsonError(error: unknown, task: Pick<Task, "id" | "worktree">): boolean {
|
|
if (error instanceof TaskDeletedError) {
|
|
return false;
|
|
}
|
|
const message = typeof error === "string"
|
|
? error
|
|
: error instanceof Error
|
|
? error.message
|
|
: "";
|
|
const match = TRANSIENT_WORKTREE_TASK_JSON_ENOENT_PATTERN.exec(message);
|
|
if (!match) {
|
|
return false;
|
|
}
|
|
const [, filePath, taskIdFromPath] = match;
|
|
if (taskIdFromPath !== task.id) {
|
|
return false;
|
|
}
|
|
if (typeof task.worktree !== "string" || task.worktree.length === 0) {
|
|
return false;
|
|
}
|
|
return isContainedByWorktree(filePath, task.worktree);
|
|
}
|